forked from Shardstone/trail-into-darkness
Added a custom calendar system
This commit is contained in:
@@ -23,7 +23,10 @@
|
||||
"Bash(find D:/repos/trail-into-darkness/Assets -name *.asmdef)",
|
||||
"Bash(grep -E \"\\\\.\\(prefab|unity\\)$\")",
|
||||
"Bash(python3:*)",
|
||||
"WebSearch"
|
||||
"WebSearch",
|
||||
"Bash(mkdir -p \"D:/repos/trail-into-darkness/Packages/com.jovian.worldtime/Runtime\")",
|
||||
"Bash(mkdir -p \"D:/repos/trail-into-darkness/Packages/com.jovian.worldtime/Editor\")",
|
||||
"Bash(mkdir -p \"D:/repos/trail-into-darkness/Packages/com.jovian.worldtime/Samples~\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,11 @@ MonoBehaviour:
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 7b5e9961dadecea4bba3be6de61909f3
|
||||
m_Address: CalendarSettings
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 7e443a969de292045b2224c779d96139
|
||||
m_Address: Assets/Art/UI/menu_bar_left.png
|
||||
m_ReadOnly: 0
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Jovian.Calendar;
|
||||
using Jovian.PopupSystem;
|
||||
using Jovian.PopupSystem.UI;
|
||||
using Jovian.SaveSystem;
|
||||
@@ -125,14 +126,16 @@ namespace Nox.Game {
|
||||
partyInventoryHandler ??= new PartyInventoryHandler(adventureData, adventureSettings);
|
||||
partyInventoryHandler.Initialize();
|
||||
|
||||
timeHandler ??= new TimeHandler(adventureSettings, adventureData);
|
||||
var calendarSettings = Addressables.LoadAssetAsync<CalendarSettings>("CalendarSettings").WaitForCompletion();
|
||||
var worldClock = new WorldClock(calendarSettings);
|
||||
timeHandler ??= new TimeHandler(adventureSettings, adventureData, worldClock);
|
||||
zoneSystem ??= new ZoneSystem(mapRef.zonesObjectHolder);
|
||||
|
||||
partyMovementHandler ??= new PartyMovementHandler(partyRef, cameraController, mapLocationsReference, platformSettings.inputSettings, zoneSystem, adventureData, adventureSettings);
|
||||
partyMovementHandler.Initialize();
|
||||
|
||||
guiReferences ??= Object.FindFirstObjectByType<GuiReferences>();
|
||||
adventureView ??= new AdventureView(gameDataState, guiReferences, inputActions, adventureData, adventureSettings);
|
||||
adventureView ??= new AdventureView(gameDataState, guiReferences, inputActions, adventureData, adventureSettings, worldClock);
|
||||
adventureView.Initialize();
|
||||
|
||||
if(partyGuiView == null && guiReferences.partyMemberSlotPrefab != null) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Jovian.Calendar;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
@@ -13,6 +14,7 @@ namespace Nox.Game {
|
||||
public class TimeHandler {
|
||||
private readonly AdventureSettings adventureSettings;
|
||||
private readonly AdventureData adventureData;
|
||||
private readonly WorldClock worldClock;
|
||||
|
||||
private float localTime;
|
||||
|
||||
@@ -23,34 +25,44 @@ namespace Nox.Game {
|
||||
(0.25f, DayPhase.Morning),
|
||||
(0.50f, DayPhase.Afternoon),
|
||||
(0.75f, DayPhase.Dusk),
|
||||
(0.90f, DayPhase.Night),
|
||||
(0.90f, DayPhase.Night)
|
||||
};
|
||||
|
||||
public TimeHandler(AdventureSettings adventureSettings, AdventureData adventureData) {
|
||||
public TimeHandler(AdventureSettings adventureSettings, AdventureData adventureData, WorldClock worldClock) {
|
||||
this.adventureSettings = adventureSettings;
|
||||
this.adventureData = adventureData;
|
||||
this.worldClock = worldClock;
|
||||
localTime = adventureData.currentTime * adventureSettings.dayLength;
|
||||
}
|
||||
|
||||
public void Tick() {
|
||||
if (!adventureData.isPartyMoving) return;
|
||||
if(!adventureData.isPartyMoving) {
|
||||
return;
|
||||
}
|
||||
|
||||
localTime += Time.deltaTime;
|
||||
|
||||
if (localTime >= adventureSettings.dayLength) {
|
||||
if(localTime >= adventureSettings.dayLength) {
|
||||
localTime -= adventureSettings.dayLength;
|
||||
adventureData.currentDay++;
|
||||
}
|
||||
|
||||
adventureData.currentTime = localTime / adventureSettings.dayLength;
|
||||
adventureData.currentDayPhase = GetPhase(adventureData.currentTime);
|
||||
var normalized = localTime / adventureSettings.dayLength;
|
||||
|
||||
worldClock.Tick(normalized);
|
||||
|
||||
adventureData.currentTime = normalized;
|
||||
adventureData.currentDayPhase = GetPhase(normalized);
|
||||
}
|
||||
|
||||
private static DayPhase GetPhase(float t) {
|
||||
var phase = DayPhase.Midnight;
|
||||
foreach (var (start, p) in PhaseThresholds) {
|
||||
if (t >= start) phase = p;
|
||||
else break;
|
||||
foreach(var (start, p) in PhaseThresholds) {
|
||||
if(t >= start) {
|
||||
phase = p;
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return phase;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Jovian.Calendar;
|
||||
using Nox.Core;
|
||||
|
||||
namespace Nox.Game.UI {
|
||||
@@ -7,20 +8,22 @@ namespace Nox.Game.UI {
|
||||
private readonly InputSystem_Actions inputActions;
|
||||
private readonly AdventureData adventureData;
|
||||
private readonly AdventureSettings adventureSettings;
|
||||
private DayPhase dayPhase = DayPhase.Midnight;
|
||||
private readonly WorldClock worldClock;
|
||||
private int currentDay;
|
||||
private int previousTime;
|
||||
|
||||
public AdventureView(
|
||||
GameDataState gameDataState,
|
||||
public AdventureView(GameDataState gameDataState,
|
||||
GuiReferences guiReferences,
|
||||
InputSystem_Actions inputActions,
|
||||
AdventureData adventureData,
|
||||
AdventureSettings adventureSettings) {
|
||||
AdventureSettings adventureSettings,
|
||||
WorldClock worldClock) {
|
||||
this.gameDataState = gameDataState;
|
||||
this.guiReferences = guiReferences;
|
||||
this.inputActions = inputActions;
|
||||
this.adventureData = adventureData;
|
||||
this.adventureSettings = adventureSettings;
|
||||
this.worldClock = worldClock;
|
||||
}
|
||||
private void InvokePauseMenu() {
|
||||
gameDataState.ChangePlayMode(PlayMode.PauseMenu);
|
||||
@@ -28,7 +31,8 @@ namespace Nox.Game.UI {
|
||||
|
||||
public void Initialize() {
|
||||
guiReferences.pauseMenuButton.onClick.AddListener(InvokePauseMenu);
|
||||
guiReferences.dayText.text = $"Day {adventureData.currentDay}, {adventureData.currentDayPhase.ToString()}";
|
||||
guiReferences.dayText.text = $"{worldClock.FullStringNamed()}";
|
||||
previousTime = -1;
|
||||
guiReferences.suppliesBar.fillAmount = (float)adventureData.suppliesAvailable / adventureSettings.maxSupplies;
|
||||
guiReferences.suppliesText.text = $"{adventureData.suppliesAvailable}/{adventureSettings.maxSupplies}";
|
||||
}
|
||||
@@ -40,14 +44,14 @@ namespace Nox.Game.UI {
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
public void Tick() {
|
||||
if(dayPhase != adventureData.currentDayPhase) {
|
||||
dayPhase = adventureData.currentDayPhase;
|
||||
guiReferences.dayText.text = $"Day {adventureData.currentDay}, {adventureData.currentDayPhase.ToString()}";
|
||||
var time = worldClock.Now.minute;
|
||||
if (time != previousTime) {
|
||||
previousTime = time;
|
||||
}
|
||||
guiReferences.dayText.text = $"{worldClock.FullStringNamed()}";
|
||||
|
||||
if(currentDay != adventureData.currentDay) {
|
||||
currentDay = adventureData.currentDay;
|
||||
guiReferences.dayText.text = $"Day {adventureData.currentDay}, {adventureData.currentDayPhase.ToString()}";
|
||||
guiReferences.suppliesBar.fillAmount = (float)adventureData.suppliesAvailable / adventureSettings.maxSupplies;
|
||||
guiReferences.suppliesText.text = $"{adventureData.suppliesAvailable}/{adventureSettings.maxSupplies}";
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 1d2424482c4843c0b7a33a9cc67411b2, type: 3}
|
||||
m_Name: AdventureSettings
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Nox.Game.AdventureSettings
|
||||
startingDate: 2026-04-12
|
||||
startingTime: 08:00:00
|
||||
dayLength: 20
|
||||
maxSupplies: 20
|
||||
partyBaseSpeed: 0.3
|
||||
|
||||
29
Assets/Database/Game/CalendarSettings.asset
Normal file
29
Assets/Database/Game/CalendarSettings.asset
Normal 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
|
||||
8
Assets/Database/Game/CalendarSettings.asset.meta
Normal file
8
Assets/Database/Game/CalendarSettings.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b5e9961dadecea4bba3be6de61909f3
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -680,6 +680,50 @@ PrefabInstance:
|
||||
propertyPath: m_LocalEulerAnglesHint.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5547799966266031405, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_fontSize
|
||||
value: 32.83
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5547799966266031405, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_fontSizeMax
|
||||
value: 32.83
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5547799966266031405, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_fontSizeMin
|
||||
value: 4.7
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5547799966266031405, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_enableAutoSizing
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5547799966266031405, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_HorizontalAlignment
|
||||
value: 2
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7031164879444458698, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_AnchorMin.x
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7031164879444458698, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_AnchorMin.y
|
||||
value: 0.5
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7031164879444458698, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_SizeDelta.x
|
||||
value: -45.73
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7031164879444458698, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_SizeDelta.y
|
||||
value: 60.911
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7031164879444458698, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_AnchoredPosition.x
|
||||
value: -24.619995
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7031164879444458698, guid: ddc1b5dd628590a4084c1997dd102f62, type: 3}
|
||||
propertyPath: m_AnchoredPosition.y
|
||||
value: 2.125
|
||||
objectReference: {fileID: 0}
|
||||
m_RemovedComponents: []
|
||||
m_RemovedGameObjects: []
|
||||
m_AddedGameObjects: []
|
||||
|
||||
8
Packages/com.jovian.calendar/Editor.meta
Normal file
8
Packages/com.jovian.calendar/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 361fc968822691543a13717f28a81b18
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f267cb10ab07ff54584ed67eb6e56e65
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
151
Packages/com.jovian.calendar/README.md
Normal file
151
Packages/com.jovian.calendar/README.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# 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
Packages/com.jovian.calendar/README.md.meta
Normal file
7
Packages/com.jovian.calendar/README.md.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b2f99c9462058d47ae5f4d667d063d4
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/com.jovian.calendar/Runtime.meta
Normal file
8
Packages/com.jovian.calendar/Runtime.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5171b66295e446142a89186f5d91bb16
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
98
Packages/com.jovian.calendar/Runtime/CalendarSettings.cs
Normal file
98
Packages/com.jovian.calendar/Runtime/CalendarSettings.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e3a6df85a820d0a4db469ae8a20ea773
|
||||
14
Packages/com.jovian.calendar/Runtime/Jovian.Calendar.asmdef
Normal file
14
Packages/com.jovian.calendar/Runtime/Jovian.Calendar.asmdef
Normal 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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 237923931c0f9cf4ea0d30f11d58dda7
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
214
Packages/com.jovian.calendar/Runtime/WorldClock.cs
Normal file
214
Packages/com.jovian.calendar/Runtime/WorldClock.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Packages/com.jovian.calendar/Runtime/WorldClock.cs.meta
Normal file
2
Packages/com.jovian.calendar/Runtime/WorldClock.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5121873037b1b704dbee5b3f060332d9
|
||||
92
Packages/com.jovian.calendar/Runtime/WorldDateTime.cs
Normal file
92
Packages/com.jovian.calendar/Runtime/WorldDateTime.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 14218276a99cfd94d805fff6fbbb621e
|
||||
16
Packages/com.jovian.calendar/package.json
Normal file
16
Packages/com.jovian.calendar/package.json
Normal 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
Packages/com.jovian.calendar/package.json.meta
Normal file
7
Packages/com.jovian.calendar/package.json.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f75f1da22b70e43499179c6452e92709
|
||||
PackageManifestImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -18,6 +18,12 @@
|
||||
"source": "embedded",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.jovian.calendar": {
|
||||
"version": "file:com.jovian.calendar",
|
||||
"depth": 0,
|
||||
"source": "embedded",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.jovian.ingame-logging": {
|
||||
"version": "file:com.jovian.ingame-logging",
|
||||
"depth": 0,
|
||||
|
||||
Reference in New Issue
Block a user