1
World Time and Calendar
minjaesong edited this page 2025-11-24 21:24:45 +09:00

World Time and Calendar

Audience: Terrarum the game maintainers working with ingame time-based systems.

Terrarum the game uses a custom calendar system tailored specifically for the ingame lore. This guide covers the calendar structure, time representation, date formatting, and working with world time programmatically.

Overview

The world time system provides:

  • Custom calendar — 4 seasons, 30 days each, no leap years
  • 8-day week — Inspired by The World Calendar
  • 24-hour clock — No AM/PM, forced 24-hour format
  • Real-time mapping — One game day = 22 real minutes
  • ISO 8601 compliance — Time intervals follow international standard
  • Predictable equinoxes — Always occur on the 15th of each month

Calendar Structure

The Yearly Calendar

A year consists of 4 seasons (months), each lasting exactly 30 days. There are no leap years.

=========================
|Mo|Ty|Mi|To|Fr|La|Su|Ve|
|--|--|--|--|--|--|--|--|
| 1| 2| 3| 4| 5| 6| 7|  | <- Spring
| 8| 9|10|11|12|13|14|  |
|15|16|17|18|19|20|21|  |
|22|23|24|25|26|27|28|  |
|29|30| 1| 2| 3| 4| 5|  | <- Summer
| 6| 7| 8| 9|10|11|12|  |
|13|14|15|16|17|18|19|  |
|20|21|22|23|24|25|26|  |
|27|28|29|30| 1| 2| 3|  | <- Autumn
| 4| 5| 6| 7| 8| 9|10|  |
|11|12|13|14|15|16|17|  |
|18|19|20|21|22|23|24|  |
|25|26|27|28|29|30| 1|  | <- Winter
| 2| 3| 4| 5| 6| 7| 8|  |
| 9|10|11|12|13|14|15|  |
|16|17|18|19|20|21|22|  |
|23|24|25|26|27|28|29|30|
=========================

Key Calendar Facts

  • Year length: 120 days
  • Week length: 7 or 8 days
  • Seasons: Spring, Summer, Autumn, Winter (30 days each)
  • Week starts: Monday (Mondag)
  • 8th day of the week: Verddag (Winter 30th) — New Year's Eve holiday
  • New Year: Spring 1st (Mondag)
  • Equinoxes/Solstices: Always on the 15th of each season

Day Names

Day Name Abbreviation
1 Mondag Mo
2 Tysdag Ty
3 Middag Mi
4 Torsdag To
5 Fredag Fr
6 Lagsdag La
7 Sundag Su
8 Verddag Ve

Note: Verddag only occurs on Winter 30th as the year's 8th-day week completion.

Season Names

Season Number Days Notes
Spring 1 1-30 Spring 1st is New Year
Summer 2 1-30
Autumn 3 1-30
Winter 4 1-30 Winter 30th is New Year's Eve (Verddag)

Time Representation

WorldTime Class

Time is managed by the WorldTime class:

class WorldTime(initTime: Long = 0L) {
    var TIME_T = 0L  // Time in seconds since epoch

    // Time components
    val seconds: Int  // 0-59
    val minutes: Int  // 0-59
    val hours: Int    // 0-23

    // Date components
    val days: Int     // 1-30
    val months: Int   // 1-4 (Spring=1, Summer=2, Autumn=3, Winter=4)
    val years: Int    // Year number

    val dayOfWeek: Int     // 0-7 (0=Mondag, 7=Verddag)
    val dayOfYear: Int     // 0-119
}

Time Constants

const val MINUTE_SEC = 60           // 60 seconds per minute
const val HOUR_MIN = 60             // 60 minutes per hour
const val HOURS_PER_DAY = 22        // 22 hours per day
const val HOUR_SEC = 3600           // 3600 seconds per hour
const val DAY_LENGTH = 79200        // 22 * 3600 seconds per day
const val YEAR_DAYS = 120           // 120 days per year

Epoch

The epoch (time zero) is:

  • Year 1, Spring 1st, 00:00:00 (Mondag)
  • Represented as: 0001-01-01 or 00010101

Date Formatting

Human-Readable Format

Format: Year-MonthName-Date

// Examples:
"0125-Spring-07"  // Year 125, Spring 7th
"0125-Summ-15"    // Year 125, Summer 15th (solstice)
"0125-Autu-22"    // Year 125, Autumn 22nd
"0125-Wint-30"    // Year 125, Winter 30th (Verddag)

Usage:

val formattedTime = worldTime.getFormattedTime()
// Returns: "0125-Spring-07"

Number-Only Format

Format: Year-Month-Date where months are numbered 1-4:

// Examples:
"0125-01-07"  // Year 125, Spring 7th
"0125-02-15"  // Year 125, Summer 15th
"0125-03-22"  // Year 125, Autumn 22nd
"0125-04-30"  // Year 125, Winter 30th

Computerised Format

Format: YearMonthDate (no separators)

// Examples:
01250107  // Year 125, Spring 7th
01250215  // Year 125, Summer 15th
01250322  // Year 125, Autumn 22nd
01250430  // Year 125, Winter 30th

Usage:

val filenameTime = worldTime.getFilenameTime()
// Returns: 01250107

Short Time Format

Abbreviated for UI display:

val shortTime = worldTime.getShortTime()
// Returns: "Spr-07" or "Wint-30"

Working with Time

Getting Current Time

val worldTime = INGAME.world.worldTime
val currentHour = worldTime.hours
val currentDay = worldTime.days
val currentMonth = worldTime.months
val currentYear = worldTime.years

Advancing Time

// Advance by 1 second
worldTime.addTime(1)

// Advance by 1 minute
worldTime.addTime(WorldTime.MINUTE_SEC)

// Advance by 1 hour
worldTime.addTime(WorldTime.HOUR_SEC)

// Advance by 1 day
worldTime.addTime(WorldTime.DAY_LENGTH)

Setting Specific Time

// Set to a specific timestamp
worldTime.TIME_T = targetTime

// Set to specific date/time
worldTime.setTime(year = 125, month = 1, day = 15, hour = 12, minute = 30)

Time Calculations

Day of Week

val dayOfWeek = worldTime.dayOfWeek
when (dayOfWeek) {
    0 -> println("Mondag")
    1 -> println("Tysdag")
    2 -> println("Middag")
    3 -> println("Torsdag")
    4 -> println("Fredag")
    5 -> println("Lagsdag")
    6 -> println("Sundag")
    7 -> println("Verddag")  // Only on Winter 30th
}

Day of Year

val dayOfYear = worldTime.dayOfYear  // 0-119
val season = dayOfYear / 30  // 0=Spring, 1=Summer, 2=Autumn, 3=Winter
val dayInSeason = (dayOfYear % 30) + 1  // 1-30

Time Until Event

// Calculate time until next sunrise (hour 6)
val currentHour = worldTime.hours
val hoursUntilSunrise = if (currentHour < 6) {
    6 - currentHour
} else {
    (22 - currentHour) + 6  // Tomorrow
}

val secondsUntilSunrise = hoursUntilSunrise * WorldTime.HOUR_SEC

Solar Cycle

Day/Night Cycle

The day lasts 22 hours with the following structure:

// Approximate solar positions
0h  - 5h    Night (dark)
6h  - 8h    Dawn (sunrise)
9h  - 13h   Day (bright)
14h - 16h   Afternoon
17h - 19h   Dusk (sunset)
20h - 21h   Evening (twilight)

Solar Angle Calculation

fun getSolarAngle(time: WorldTime): Double {
    val hourAngle = (time.hours + time.minutes / 60.0) / 22.0
    return hourAngle * 2 * Math.PI
}

fun isDaytime(time: WorldTime): Boolean {
    return time.hours in 6..18
}

fun isNighttime(time: WorldTime): Boolean {
    return time.hours < 6 || time.hours > 18
}

Seasonal Effects

Season Detection

fun getCurrentSeason(time: WorldTime): Season {
    return when (time.months) {
        1 -> Season.SPRING
        2 -> Season.SUMMER
        3 -> Season.AUTUMN
        4 -> Season.WINTER
        else -> Season.SPRING
    }
}

enum class Season {
    SPRING, SUMMER, AUTUMN, WINTER
}

Equinoxes and Solstices

fun isEquinoxOrSolstice(time: WorldTime): Boolean {
    return time.days == 15
}

fun getCelestialEvent(time: WorldTime): String? {
    if (time.days != 15) return null

    return when (time.months) {
        1 -> "Spring Equinox"
        2 -> "Summer Solstice"
        3 -> "Autumn Equinox"
        4 -> "Winter Solstice"
        else -> null
    }
}

Real-Time Mapping

Game Time vs. Real Time

One game day = 22 real minutes

const val REAL_SECONDS_PER_GAME_DAY = 22 * 60  // 1320 real seconds
const val GAME_SECONDS_PER_REAL_SECOND = WorldTime.DAY_LENGTH / REAL_SECONDS_PER_GAME_DAY
// = 86400 / 1320 ≈ 65.45

// One real second ≈ 65 game seconds = 1 game minute

Time Scale Calculations

// Convert real seconds to game seconds
fun realToGameTime(realSeconds: Float): Long {
    return (realSeconds * GAME_SECONDS_PER_REAL_SECOND).toLong()
}

// Convert game seconds to real seconds
fun gameToRealTime(gameSeconds: Long): Float {
    return gameSeconds / GAME_SECONDS_PER_REAL_SECOND.toFloat()
}

// Example: 5 real minutes
val fiveRealMinutes = 5 * 60  // 300 real seconds
val gameTime = realToGameTime(300f)  // 18000 game seconds = 5 game hours

Time-Based Events

Scheduling Events

class ScheduledEvent(
    val triggerTime: Long,
    val action: (WorldTime) -> Unit
)

val eventQueue = PriorityQueue<ScheduledEvent> { a, b ->
    a.triggerTime.compareTo(b.triggerTime)
}

// Schedule event
fun scheduleEvent(worldTime: WorldTime, hoursFromNow: Int, action: (WorldTime) -> Unit) {
    val triggerTime = worldTime.TIME_T + (hoursFromNow * WorldTime.HOUR_SEC)
    eventQueue.add(ScheduledEvent(triggerTime, action))
}

// Process events
fun processEvents(worldTime: WorldTime) {
    while (eventQueue.isNotEmpty() && eventQueue.peek().triggerTime <= worldTime.TIME_T) {
        val event = eventQueue.poll()
        event.action(worldTime)
    }
}

Daily Events

// Check if it's a specific hour
fun checkDailyEvent(worldTime: WorldTime, targetHour: Int, action: () -> Unit) {
    if (worldTime.hours == targetHour && worldTime.minutes == 0 && worldTime.seconds == 0) {
        action()
    }
}

// Example: Daily shop restock at 6:00
if (worldTime.hours == 6 && worldTime.minutes == 0) {
    restockShop()
}

Seasonal Events

// Check for season start
fun checkSeasonChange(worldTime: WorldTime, previousMonth: Int) {
    if (worldTime.months != previousMonth && worldTime.days == 1) {
        onSeasonChange(getCurrentSeason(worldTime))
    }
}

// Example: Seasonal weather change
fun onSeasonChange(season: Season) {
    when (season) {
        Season.SPRING -> weatherSystem.setSpringWeather()
        Season.SUMMER -> weatherSystem.setSummerWeather()
        Season.AUTUMN -> weatherSystem.setAutumnWeather()
        Season.WINTER -> weatherSystem.setWinterWeather()
    }
}

Serialisation

Saving Time

fun saveTime(worldTime: WorldTime): ByteArray {
    return worldTime.TIME_T.toByteArray()
}

Loading Time

fun loadTime(data: ByteArray): WorldTime {
    val timeT = data.toLong()
    return WorldTime(timeT)
}

Best Practises

  1. Use WorldTime instance — Don't calculate time manually
  2. Check equinoxes on day 15 — Predictable solar events
  3. Handle Verddag correctly — 8th day only on Winter 30th
  4. Use constants — Don't hardcode time values
  5. Schedule via TIME_T — Use absolute timestamps for events
  6. Format consistently — Use provided formatting functions
  7. Test year boundaries — Ensure proper rollover from Winter 30 to Spring 1
  8. Consider time zones — Game uses single global time
  9. Handle pause — Time stops when game is paused
  10. Sync with world save — Always serialise TIME_T

Common Pitfalls

  • Assuming 24-hour days — Days are 22 hours
  • Forgetting Verddag — 8th day only occurs once per year
  • Hardcoding month numbers — Use season constants
  • Not handling year rollover — Winter 30 → Spring 1
  • Assuming AM/PM — 24-hour clock enforced
  • Miscalculating real-time mapping — 1 real second = 1 game minute
  • Ignoring equinox timing — Always on 15th, not varying
  • Using wall-clock time — Game time is independent
  • Forgetting to update events — Process event queue regularly
  • Not serialising properly — TIME_T must be saved

See Also

  • World — World generation and management
  • Actors — Time-based actor behaviour
  • Weather — Seasonal weather systems
  • Modules-Setup — Module time integration