diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..5a74284 --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 361fc968822691543a13717f28a81b18 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Jovian.Calendar.Editor.asmdef b/Editor/Jovian.Calendar.Editor.asmdef new file mode 100644 index 0000000..9a1d75b --- /dev/null +++ b/Editor/Jovian.Calendar.Editor.asmdef @@ -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 +} diff --git a/Editor/Jovian.Calendar.Editor.asmdef.meta b/Editor/Jovian.Calendar.Editor.asmdef.meta new file mode 100644 index 0000000..be19363 --- /dev/null +++ b/Editor/Jovian.Calendar.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f267cb10ab07ff54584ed67eb6e56e65 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md index ff5730c..2386942 100644 --- a/README.md +++ b/README.md @@ -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. \ No newline at end of file +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` and `IComparable` 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").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 | diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..dcbc472 --- /dev/null +++ b/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3b2f99c9462058d47ae5f4d667d063d4 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime.meta b/Runtime.meta new file mode 100644 index 0000000..ad52140 --- /dev/null +++ b/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5171b66295e446142a89186f5d91bb16 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CalendarSettings.cs b/Runtime/CalendarSettings.cs new file mode 100644 index 0000000..d9338da --- /dev/null +++ b/Runtime/CalendarSettings.cs @@ -0,0 +1,98 @@ +using System; +using UnityEngine; + +namespace Jovian.Calendar { + /// + /// 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. + /// + [CreateAssetMenu(fileName = "CalendarSettings", menuName = "Jovian/Calendar/Calendar Settings")] + public class CalendarSettings : ScriptableObject { + /// How many real-time seconds one full 0 to 1 float cycle represents. + public float secondsPerFullDay; + + /// How many in-world hours per day (e.g. 24, or 16 for a shorter day). + public int hoursPerDay; + + /// How many in-world minutes per hour (e.g. 60). + public int minutesPerHour; + + /// + /// Days in each month, in order. Length of this array defines months per year. + /// E.g. { 30, 28, 31 } = 3 months with different lengths. + /// + public int[] daysPerMonth; + + /// + /// Custom month names. Must match daysPerMonth.Length. + /// + public string[] monthNames; + + /// Optional: days per week for week-tracking. 0 to disable. + public int daysPerWeek; + + // --- Starting date --- + public int startYear; + public int startMonth; // 0-indexed + public int startDay; // 0-indexed + public int startHour; + public int startMinute; + + /// Number of months in a year, derived from daysPerMonth array length. + public int MonthsPerYear => daysPerMonth?.Length ?? 0; + + /// Total days in one full year. + public int TotalDaysInYear { + get { + var total = 0; + if(daysPerMonth == null) { + return 0; + } + foreach(var t in daysPerMonth) { + total += t; + } + return total; + } + } + + /// Total in-world minutes in a single day. + public int MinutesPerDay => hoursPerDay * minutesPerHour; + + /// Validates all calendar parameters. Throws ArgumentException on invalid configuration. + 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"); + } + } + } +} diff --git a/Runtime/CalendarSettings.cs.meta b/Runtime/CalendarSettings.cs.meta new file mode 100644 index 0000000..67a05b5 --- /dev/null +++ b/Runtime/CalendarSettings.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e3a6df85a820d0a4db469ae8a20ea773 \ No newline at end of file diff --git a/Runtime/Jovian.Calendar.asmdef b/Runtime/Jovian.Calendar.asmdef new file mode 100644 index 0000000..29428f6 --- /dev/null +++ b/Runtime/Jovian.Calendar.asmdef @@ -0,0 +1,14 @@ +{ + "name": "Jovian.Calendar", + "rootNamespace": "Jovian.Calendar", + "references": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Runtime/Jovian.Calendar.asmdef.meta b/Runtime/Jovian.Calendar.asmdef.meta new file mode 100644 index 0000000..b970d00 --- /dev/null +++ b/Runtime/Jovian.Calendar.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 237923931c0f9cf4ea0d30f11d58dda7 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/WorldClock.cs b/Runtime/WorldClock.cs new file mode 100644 index 0000000..c751afd --- /dev/null +++ b/Runtime/WorldClock.cs @@ -0,0 +1,214 @@ +namespace Jovian.Calendar { + /// + /// Core calendar clock. Feed it a 0-1 normalized day float each tick. + /// Internally accumulates world-minutes and rolls them into the calendar. + /// + public class WorldClock { + /// The calendar configuration driving this clock. + public CalendarSettings Settings { get; } + + /// The current world date and time. + public WorldDateTime Now { get; private set; } + + /// + /// Total in-world minutes elapsed since the start date. + /// This is the single source of truth. WorldDateTime is derived from it. + /// + public long TotalElapsedMinutes { get; private set; } + + /// The normalized time (0-1) from the last tick. + public float LastNormalizedTime { get; private set; } + + private float prevNormalized; + private bool initialized; + private float fractionalMinutes; + private readonly long startOffsetMinutes; + + /// + /// Creates a new world clock from the given calendar settings. + /// Validates settings and initializes to the configured start date. + /// + 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); + } + + /// + /// 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). + /// + 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; + } + + /// + /// Manually advance the clock by a number of in-world minutes. + /// Useful for sleep/rest skips, cutscenes, etc. + /// + 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; + } + + /// + /// Converts a total-minutes-from-epoch value into a WorldDateTime. + /// + 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 + }; + } + + /// + /// Inverse of MakeDateTime: given a calendar date, produce total minutes from epoch. + /// + 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; + } + + /// + /// Get the current day-of-week (0-indexed), or -1 if weeks are disabled. + /// + public int DayOfWeek { + get { + if(Settings.daysPerWeek <= 0) { + return -1; + } + var totalDays = (startOffsetMinutes + TotalElapsedMinutes) / Settings.MinutesPerDay; + return (int)(totalDays % Settings.daysPerWeek); + } + } + + /// + /// Normalized time of day as 0-1 float, derived from the current WorldDateTime. + /// Useful for lighting, skybox lerp, etc. + /// + public float NormalizedTimeOfDay { + get { + float currentMinute = (Now.hour * Settings.minutesPerHour) + Now.minute; + return currentMinute / Settings.MinutesPerDay; + } + } + + /// Returns the name of the current month from settings. + public string GetMonthName() { + var month = Now.month; + if(Settings.monthNames == null || month < 0 || month >= Settings.monthNames.Length) { + return "???"; + } + return Settings.monthNames[month]; + } + + /// Returns a full display string with named month and ordinal day. + 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; + } + } +} diff --git a/Runtime/WorldClock.cs.meta b/Runtime/WorldClock.cs.meta new file mode 100644 index 0000000..12fef21 --- /dev/null +++ b/Runtime/WorldClock.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5121873037b1b704dbee5b3f060332d9 \ No newline at end of file diff --git a/Runtime/WorldDateTime.cs b/Runtime/WorldDateTime.cs new file mode 100644 index 0000000..695b98a --- /dev/null +++ b/Runtime/WorldDateTime.cs @@ -0,0 +1,92 @@ +using System; + +namespace Jovian.Calendar { + /// + /// Represents a point in world-calendar time. Display-friendly. + /// All fields are 0-indexed internally; display properties are 1-indexed where expected. + /// + [Serializable] + public struct WorldDateTime : IEquatable, IComparable { + public int year; + public int month; // 0-indexed + public int day; // 0-indexed + public int hour; + public int minute; + + /// 1-indexed day for display. + public int DisplayDay => day + 1; + + /// 1-indexed month for display. + public int DisplayMonth => month + 1; + + /// Returns "HH:MM" style time string. + public string TimeString => $"{hour:D2}:{minute:D2}"; + + /// Returns "Day/Month/Year" display string (1-indexed). + public string DateString => $"{DisplayDay}/{DisplayMonth}/{year}"; + + /// Returns full "Day/Month/Year HH:MM". + 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; + } + } +} diff --git a/Runtime/WorldDateTime.cs.meta b/Runtime/WorldDateTime.cs.meta new file mode 100644 index 0000000..6b20c3c --- /dev/null +++ b/Runtime/WorldDateTime.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 14218276a99cfd94d805fff6fbbb621e \ No newline at end of file diff --git a/Samples~/README.md b/Samples~/README.md new file mode 100644 index 0000000..188613d --- /dev/null +++ b/Samples~/README.md @@ -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)` diff --git a/Samples~/Settings/CalendarSettings.asset b/Samples~/Settings/CalendarSettings.asset new file mode 100644 index 0000000..be9df1c --- /dev/null +++ b/Samples~/Settings/CalendarSettings.asset @@ -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 diff --git a/package.json b/package.json new file mode 100644 index 0000000..847ff68 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..5bc36e4 --- /dev/null +++ b/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f75f1da22b70e43499179c6452e92709 +PackageManifestImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: