files from the project

This commit is contained in:
Sebastian Bularca
2026-04-12 19:21:21 +02:00
parent 561ce6eea9
commit 467dab875b
18 changed files with 693 additions and 2 deletions

8
Editor.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 361fc968822691543a13717f28a81b18
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,16 @@
{
"name": "Jovian.Calendar.Editor",
"rootNamespace": "Jovian.Calendar.Editor",
"references": [
"Jovian.Calendar"
],
"includePlatforms": ["Editor"],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f267cb10ab07ff54584ed67eb6e56e65
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

150
README.md
View File

@@ -1,3 +1,151 @@
# unity-calendar-system
# Jovian Calendar
A configurable in-game calendar and world clock system for Unity. Supports custom day lengths, hours, minutes, months with variable lengths, named months, week tracking, and a serializable date-time struct. No external dependencies.
## Requirements
- Unity 2022.3 or later
- No additional package dependencies
## Quick Start
### 1. Create a CalendarSettings asset
In the Unity Editor, go to **Assets > Create > Jovian > Calendar > Calendar Settings**.
Configure:
- **Seconds Per Full Day**: How many real-time seconds equal one full in-game day (the 0-1 cycle)
- **Hours Per Day**: In-world hours per day (e.g. 24)
- **Minutes Per Hour**: In-world minutes per hour (e.g. 60)
- **Days Per Month**: Array defining the length of each month. Array length = months per year
- **Month Names**: Display names for each month (must match Days Per Month length)
- **Days Per Week**: Optional week length (0 to disable)
- **Start Date**: Starting year, month, day, hour, minute for the clock
### 2. Create and tick a WorldClock
```csharp
using Jovian.Calendar;
// Create the clock from settings
var clock = new WorldClock(calendarSettings);
// Each frame, feed it a 0-1 normalized day progress
clock.Tick(normalizedDayTime);
// Query the current date and time
WorldDateTime now = clock.Now;
Debug.Log($"Year {now.year}, {clock.GetMonthName()} {now.DisplayDay}, {now.TimeString}");
```
### 3. Display the date
```csharp
// Individual fields
int year = clock.Now.year;
int month = clock.Now.DisplayMonth; // 1-indexed
int day = clock.Now.DisplayDay; // 1-indexed
string time = clock.Now.TimeString; // "HH:MM"
// Full display string
string full = clock.FullStringNamed();
// e.g. "14:30, 5th day of Frostmoon, year 3"
```
## CalendarSettings
ScriptableObject defining calendar rules. Create via **Assets > Create > Jovian > Calendar > Calendar Settings**.
| Field | Type | Description |
|---|---|---|
| `secondsPerFullDay` | float | Real-time seconds for one full 0-1 day cycle |
| `hoursPerDay` | int | In-world hours per day |
| `minutesPerHour` | int | In-world minutes per hour |
| `daysPerMonth` | int[] | Days in each month. Array length = months per year |
| `monthNames` | string[] | Display name per month. Must match daysPerMonth length |
| `daysPerWeek` | int | Days per week (0 to disable week tracking) |
| `startYear` | int | Starting year |
| `startMonth` | int | Starting month (0-indexed) |
| `startDay` | int | Starting day (0-indexed) |
| `startHour` | int | Starting hour |
| `startMinute` | int | Starting minute |
**Computed properties:**
- `MonthsPerYear` -- derived from daysPerMonth array length
- `TotalDaysInYear` -- sum of all daysPerMonth entries
- `MinutesPerDay` -- hoursPerDay * minutesPerHour
## WorldClock
The core clock class. Feed it a normalized 0-1 day progress each tick. It accumulates world-minutes internally, handling day wrap-around and fractional minute accumulation across frames.
```csharp
var clock = new WorldClock(settings);
// Tick with normalized time (0 = midnight, 0.5 = noon, 1 = next midnight)
clock.Tick(normalizedTime);
// Manual time skip (sleep, rest, cutscene)
clock.AdvanceMinutes(480); // skip 8 hours
// Queries
WorldDateTime now = clock.Now;
long elapsed = clock.TotalElapsedMinutes;
float normalized = clock.NormalizedTimeOfDay; // 0-1 for lighting/skybox
int weekday = clock.DayOfWeek; // 0-indexed, -1 if disabled
string monthName = clock.GetMonthName();
string display = clock.FullStringNamed();
```
## WorldDateTime
A serializable struct representing a point in calendar time. Implements `IEquatable<WorldDateTime>` and `IComparable<WorldDateTime>` for collections and sorting.
```csharp
WorldDateTime dt = clock.Now;
// 0-indexed fields
dt.year; dt.month; dt.day; dt.hour; dt.minute;
// 1-indexed display properties
dt.DisplayDay; // day + 1
dt.DisplayMonth; // month + 1
// Formatting
dt.TimeString; // "14:30"
dt.DateString; // "5/3/1"
dt.FullString; // "5/3/1 14:30"
// Comparison
if(dateA < dateB) { ... }
if(dateA == dateB) { ... }
var sorted = dates.OrderBy(d => d);
```
## Integration Example
```csharp
// In your game state initialization:
var calendarSettings = Addressables.LoadAssetAsync<CalendarSettings>("CalendarSettings").WaitForCompletion();
var worldClock = new WorldClock(calendarSettings);
// In your time handler's Tick():
float normalizedTime = localTime / dayLength;
worldClock.Tick(normalizedTime);
// Update UI:
dayText.text = $"Day {worldClock.Now.DisplayDay}, {worldClock.GetMonthName()}";
// Use NormalizedTimeOfDay for visual effects:
float t = worldClock.NormalizedTimeOfDay;
skyboxMaterial.SetFloat("_Blend", t);
directionalLight.intensity = sunCurve.Evaluate(t);
```
## API Reference
| Type | Description |
|---|---|
| `CalendarSettings` | ScriptableObject defining calendar rules (day length, months, weeks, start date) |
| `WorldClock` | Core clock class. Tick with normalized time, query WorldDateTime |
| `WorldDateTime` | Serializable struct for date-time values. IEquatable, IComparable, operator overloads |

7
README.md.meta Normal file
View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3b2f99c9462058d47ae5f4d667d063d4
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
Runtime.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5171b66295e446142a89186f5d91bb16
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,98 @@
using System;
using UnityEngine;
namespace Jovian.Calendar {
/// <summary>
/// ScriptableObject defining the rules of an in-game calendar: day length, hours, minutes,
/// month structure, month names, week length, and starting date. Create via
/// Assets > Create > Jovian > Calendar > Calendar Settings.
/// </summary>
[CreateAssetMenu(fileName = "CalendarSettings", menuName = "Jovian/Calendar/Calendar Settings")]
public class CalendarSettings : ScriptableObject {
/// <summary>How many real-time seconds one full 0 to 1 float cycle represents.</summary>
public float secondsPerFullDay;
/// <summary>How many in-world hours per day (e.g. 24, or 16 for a shorter day).</summary>
public int hoursPerDay;
/// <summary>How many in-world minutes per hour (e.g. 60).</summary>
public int minutesPerHour;
/// <summary>
/// Days in each month, in order. Length of this array defines months per year.
/// E.g. { 30, 28, 31 } = 3 months with different lengths.
/// </summary>
public int[] daysPerMonth;
/// <summary>
/// Custom month names. Must match daysPerMonth.Length.
/// </summary>
public string[] monthNames;
/// <summary>Optional: days per week for week-tracking. 0 to disable.</summary>
public int daysPerWeek;
// --- Starting date ---
public int startYear;
public int startMonth; // 0-indexed
public int startDay; // 0-indexed
public int startHour;
public int startMinute;
/// <summary>Number of months in a year, derived from daysPerMonth array length.</summary>
public int MonthsPerYear => daysPerMonth?.Length ?? 0;
/// <summary>Total days in one full year.</summary>
public int TotalDaysInYear {
get {
var total = 0;
if(daysPerMonth == null) {
return 0;
}
foreach(var t in daysPerMonth) {
total += t;
}
return total;
}
}
/// <summary>Total in-world minutes in a single day.</summary>
public int MinutesPerDay => hoursPerDay * minutesPerHour;
/// <summary>Validates all calendar parameters. Throws ArgumentException on invalid configuration.</summary>
public void Validate() {
if(secondsPerFullDay <= 0f) {
throw new ArgumentException("SecondsPerFullDay must be > 0");
}
if(hoursPerDay <= 0) {
throw new ArgumentException("HoursPerDay must be > 0");
}
if(minutesPerHour <= 0) {
throw new ArgumentException("MinutesPerHour must be > 0");
}
if(daysPerMonth == null || daysPerMonth.Length == 0) {
throw new ArgumentException("DaysPerMonth must have at least one entry");
}
if(monthNames == null || monthNames.Length != daysPerMonth.Length) {
throw new ArgumentException("MonthNames length must match DaysPerMonth length");
}
for(var i = 0; i < daysPerMonth.Length; i++) {
if(daysPerMonth[i] <= 0) {
throw new ArgumentException($"DaysPerMonth[{i}] must be > 0");
}
}
if(startMonth < 0 || startMonth >= daysPerMonth.Length) {
throw new ArgumentException("StartMonth out of range");
}
if(startDay < 0 || startDay >= daysPerMonth[startMonth]) {
throw new ArgumentException("StartDay out of range for StartMonth");
}
if(startHour < 0 || startHour >= hoursPerDay) {
throw new ArgumentException("StartHour out of range");
}
if(startMinute < 0 || startMinute >= minutesPerHour) {
throw new ArgumentException("StartMinute out of range");
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e3a6df85a820d0a4db469ae8a20ea773

View File

@@ -0,0 +1,14 @@
{
"name": "Jovian.Calendar",
"rootNamespace": "Jovian.Calendar",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 237923931c0f9cf4ea0d30f11d58dda7
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

214
Runtime/WorldClock.cs Normal file
View File

@@ -0,0 +1,214 @@
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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5121873037b1b704dbee5b3f060332d9

92
Runtime/WorldDateTime.cs Normal file
View File

@@ -0,0 +1,92 @@
using System;
namespace Jovian.Calendar {
/// <summary>
/// Represents a point in world-calendar time. Display-friendly.
/// All fields are 0-indexed internally; display properties are 1-indexed where expected.
/// </summary>
[Serializable]
public struct WorldDateTime : IEquatable<WorldDateTime>, IComparable<WorldDateTime> {
public int year;
public int month; // 0-indexed
public int day; // 0-indexed
public int hour;
public int minute;
/// <summary>1-indexed day for display.</summary>
public int DisplayDay => day + 1;
/// <summary>1-indexed month for display.</summary>
public int DisplayMonth => month + 1;
/// <summary>Returns "HH:MM" style time string.</summary>
public string TimeString => $"{hour:D2}:{minute:D2}";
/// <summary>Returns "Day/Month/Year" display string (1-indexed).</summary>
public string DateString => $"{DisplayDay}/{DisplayMonth}/{year}";
/// <summary>Returns full "Day/Month/Year HH:MM".</summary>
public string FullString => $"{DateString} {TimeString}";
public bool Equals(WorldDateTime other) {
return year == other.year && month == other.month && day == other.day
&& hour == other.hour && minute == other.minute;
}
public int CompareTo(WorldDateTime other) {
var c = year.CompareTo(other.year);
if(c != 0) {
return c;
}
c = month.CompareTo(other.month);
if(c != 0) {
return c;
}
c = day.CompareTo(other.day);
if(c != 0) {
return c;
}
c = hour.CompareTo(other.hour);
if(c != 0) {
return c;
}
return minute.CompareTo(other.minute);
}
public override bool Equals(object obj) {
return obj is WorldDateTime other && Equals(other);
}
public override int GetHashCode() {
return HashCode.Combine(year, month, day, hour, minute);
}
public override string ToString() {
return FullString;
}
public static bool operator ==(WorldDateTime a, WorldDateTime b) {
return a.Equals(b);
}
public static bool operator !=(WorldDateTime a, WorldDateTime b) {
return !a.Equals(b);
}
public static bool operator <(WorldDateTime a, WorldDateTime b) {
return a.CompareTo(b) < 0;
}
public static bool operator >(WorldDateTime a, WorldDateTime b) {
return a.CompareTo(b) > 0;
}
public static bool operator <=(WorldDateTime a, WorldDateTime b) {
return a.CompareTo(b) <= 0;
}
public static bool operator >=(WorldDateTime a, WorldDateTime b) {
return a.CompareTo(b) >= 0;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 14218276a99cfd94d805fff6fbbb621e

14
Samples~/README.md Normal file
View File

@@ -0,0 +1,14 @@
# Calendar Samples
## Settings
| Asset | Description |
|---|---|
| `CalendarSettings` | Example calendar configuration with fantasy month names and a 10-second day cycle |
## How to use
1. Import the samples via the Unity Package Manager (select the package, expand Samples, click Import)
2. Copy the CalendarSettings asset into your project
3. Adjust the values to match your game's calendar (day length, months, month names, etc.)
4. Load via Addressables or direct reference and pass to `new WorldClock(settings)`

View File

@@ -0,0 +1,29 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e3a6df85a820d0a4db469ae8a20ea773, type: 3}
m_Name: CalendarSettings
m_EditorClassIdentifier: Assembly-CSharp::Nox.Game.CalendarSettings
secondsPerFullDay: 60
hoursPerDay: 24
minutesPerHour: 60
daysPerMonth: 5a0000005a0000005a0000005a000000
monthNames:
- Ashveil
- Thornmere
- Duskhollow
- 'Frosthollow '
daysPerWeek: 7
startYear: 4232
startMonth: 1
startDay: 0
startHour: 8
startMinute: 0

16
package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "com.jovian.calendar",
"version": "0.1.0",
"displayName": "Jovian Calendar",
"description": "A configurable in-game calendar and world clock system with custom months, day lengths, week tracking, and serializable date-time.",
"unity": "2022.3",
"keywords": [
"calendar",
"time",
"clock",
"date"
],
"author": {
"name": "Jovian"
}
}

7
package.json.meta Normal file
View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f75f1da22b70e43499179c6452e92709
PackageManifestImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: