Skip to content

Time, Scheduling & Reactivity

Advancing game time, calendars and clocks, scheduling future events, and reactive updates that follow state changes.

Time

Tracks in-game time and day count. Fires @on(dayAdvance) when the day rolls over.


(time-periods: name, ...)

Defines the named periods of day in order. Call in GameInit. Accepts two forms:

Names-only form: each period has a duration of 1 (advances by one step per (time-advance:) call):

ana
(time-periods: "morning", "afternoon", "evening", "night")
// Each period has duration 1 (step of 1 per (time-advance:) call)

Alternating name/hours form: names alternate with hour-durations:

ana
(time-periods: "Dawn", 2, "Day", 8, "Dusk", 2, "Night", 12)
// Names alternate with hour-durations

In the alternating form, (time-advance: N) consumes N hours and may advance through multiple periods if a period's duration is exhausted.

ana
// Full example:
(time-periods: "dawn", "morning", "midday", "afternoon", "dusk", "evening", "night")

If not called, the engine defaults to four periods: "morning", "afternoon", "evening", "night".


(time-mode: mode)

Sets the time progression model. Call in GameInit. Accepts cycle or linear.

  • cycle (default): Time advances through named periods. $world.timeOfDay is a period name string.
  • linear: Time advances in numeric units (hours). $world.timeOfDay is a number.
ana
(time-mode: cycle)

(time-start: value)

Sets the starting value of $world.timeOfDay for linear mode. Call in GameInit after (time-mode: linear).

ana
(time-mode: linear)
(time-start: 7)    // game begins at hour 7

Has no meaningful effect in cycle mode (a warning is logged if called in cycle mode).


(time-advance: hours)

Advances time by a number of hours (or period steps in cycle mode). When the day threshold is crossed, @on(dayAdvance) fires. Also ticks all active duration-based status effects: applies dot (recurring) modifiers for each tick they are active, then removes expired statuses and reverses their effect (flat) modifiers (see Status Effects).

ana
(time-advance: 2)     // 2 hours pass
(time-advance: 8)     // sleep through the night

(time-set-period: "period")

Sets the time of day directly to one of your defined periods. Calling (time-set-period:) fires the periodChange event, which triggers any @on(periodChange) passages and scheduled entries, just as (time-advance:) does when the period changes naturally.

ana
(time-set-period: "evening")
(time-set-period: "dawn")

(time-set: hour)

Sets the clock to a specific hour. Only valid when linear mode is enabled. Takes a numeric hour value.

ana
(time-set: 23)
(time-set: 6)

Reading time

Time of day is available as $world.timeOfDay and day count as $world.days.

ana
(if: $world.timeOfDay is "night")[
    The bar is nearly empty.
]

(if: $world.days >= 7)[
    A week has passed.
]

(time-calendar:): calendar configuration

Configures the in-game calendar so the engine can compute day-of-week names and display formatted dates in the #ana-clock HUD. Call once in GameInit.

ISO date form: use when you know the exact start date:

ana
(time-calendar: "2026-05-23")

The weekday (Saturday) is inferred automatically. The year is stored internally but never displayed.

Named-day form: use when you want a weekday name without a real calendar date:

ana
(time-calendar: month, 5, day, 23, startDay, "Tuesday")

Both forms accept an optional display keyword that controls what the HUD date line shows:

ValueHUD date line
"date" (default)Saturday, May 23
"day"Day 1, Day 2, …
"date+day"Saturday, May 23 · Day 1
"weekday"Saturday
ana
(time-calendar: "2026-05-23", display, "date+day")
(time-calendar: month, 5, day, 23, startDay, "Thursday", display, "weekday")

If (time-calendar:) is never called, the date line is hidden. The time/period portion still shows when periods or linear mode are configured.


(clock-format: format)

Sets the display format for the time portion of the #ana-clock HUD. Does not affect $world.timeOfDay.

  • "12h" (default): 9:30 AM
  • "24h": 09:30
ana
(clock-format: "24h")

Also usable in a settings screen to let players override the author default.


(date-format: format)

Sets how calendar dates appear in the #ana-clock HUD.

  • "US" (default): Saturday, May 23
  • "EU": Saturday, 23 May
  • "ISO": Saturday, 05-23
ana
(date-format: "EU")

Also usable in a settings screen.

Scheduling

The scheduling system fires effects automatically when the time period changes or a clock-hour boundary is crossed. Use it to move NPCs between locations, trigger world events, and manage time-based state without putting time checks in every passage.

All macros use positional pairs, with no colons after keys:

ana
(schedule: type, "id", matchKey, matchValue, effectKey, effectValue)
(schedule: type, "id", matchKey, matchValue, effectKey, effectValue, condition, <expr>)
(schedule-clear: type, "id")

(schedule: npc, "id", period, "name", location, "value")

Registers an NPC location schedule. When the period transitions to name, sets $npc.<id>.location to value.

ana
:: GameInit @system(init)

(time-periods: "morning", "afternoon", "evening", "night")

(schedule: npc, "bartender", period, "morning",   location, "home")
(schedule: npc, "bartender", period, "afternoon", location, "bar")
(schedule: npc, "bartender", period, "evening",   location, "bar")
(schedule: npc, "bartender", period, "night",     location, "home")

(schedule: event, "id", period, "name", trigger, "PassageName")

Registers a world event trigger. When the period becomes name, the named passage executes as an action.

ana
(schedule: event, "food_truck", period, "morning", trigger, "FoodTruck_Arrive")
(schedule: event, "food_truck", period, "evening", trigger, "FoodTruck_Leave")

Conditional entries

Add condition, <expr> at the end of any schedule call. The entry only fires when the condition is true at transition time:

ana
// Bartender stays at bar all night during a crisis
(schedule: npc, "bartender", period, "night", location, "bar",
    condition, $world.crisisLevel >= 5)

(schedule: npc, "id", hours, [start,end], location, "value")

(schedule: event, "id", hours, [start,end], trigger, "PassageName")

Hour-range entries for linear (numeric) time mode. The effect fires when world.timeOfDay crosses the start hour. [9,11] means start hour 9, understood active through hour 11.

ana
// Full workday — three non-overlapping ranges
(schedule: npc, "bartender", hours, [9,11],  location, "office")
(schedule: npc, "bartender", hours, [12,12], location, "restaurant")
(schedule: npc, "bartender", hours, [13,17], location, "office")

The engine throws an error at registration time if any two hour ranges for the same group overlap.


(schedule-clear: type, "id")

Removes all schedule entries for the given type + id group:

ana
// The bar burns down — remove its event schedule
(schedule-clear: event, "bar_karaoke")
(schedule-clear: event, "food_truck")
(set: $world.barDestroyed to true)

Rewrite the schedule after clearing if needed:

ana
(schedule-clear: npc, "bartender")
(schedule: npc, "bartender", period, "morning",   location, "ruins")
(schedule: npc, "bartender", period, "afternoon", location, "ruins")

@on(periodChange): passage-level period response

Any passage marked @on(periodChange) executes automatically on every period transition, after scheduled entries have fired:

ana
:: World_OnPeriodChange @on(periodChange)

(if: $world.timeOfDay is "night")[
    (set: $world.shopOpen to false)
]
(elseif: $world.timeOfDay is "morning")[
    (set: $world.shopOpen to true)
    (notify: "The shops are open.")
]

Day-of-week scheduling

Requires (time-calendar:) to be configured in GameInit.

day match type: fires once when (time-advance:) rolls the day over to the named weekday:

ana
(schedule: npc, "taco_truck", day, "Tuesday", location, "main_street")
(schedule: event, "market",   day, "Saturday", trigger, "MarketOpen")

day qualifier for period or hours entries: restricts a period entry to fire only on listed weekdays:

ana
// Taco truck parks on Main Street for lunch, but only Tuesday and Thursday
(schedule: npc, "taco_truck", period, "noon", location, "main_street",
    day, "Tuesday")

// Multiple days: pass an array
(schedule: npc, "taco_truck", period, "noon", location, "main_street",
    day, ["Tuesday","Thursday"])

day and condition can both be present on the same entry:

ana
(schedule: event, "market", period, "morning", trigger, "MarketOpen",
    day, "Saturday", condition, $world.marketCancelled is false)

When no calendar is configured, day qualifiers are ignored and the entry fires unconditionally.


Schedule and save/load

Schedule entries are part of the game save. Entries added during GameInit are re-registered on every load; entries added or cleared at runtime are saved and restored.

Tip: Put your default schedule in GameInit. Modify it at runtime only in response to permanent story events (a business closing, an NPC dying, etc.).

Timing & Reactivity

Ana does not have a (live:) macro. The four primitives below are all bounded and auto-cancel when their host zone is rebuilt, the passage is navigated away from, or the render generation is superseded. They cannot leak past zone teardown.


(after: duration)[content]

One-shot delayed render. Content is appended to the current zone after the given duration. Cannot loop or reschedule itself.

Duration accepts ms, s, or a unit-less integer (treated as milliseconds).

ana
The door creaks open.

(after: 1.5s)[
    A figure steps inside.
]

(every: duration, max: N)[content]

Re-renders content on an interval. The max: count is required; the macro will not register without it. The engine also enforces an upper bound (default 60 iterations) regardless of the author-supplied max:.

ana
(every: 500ms, max: 6)[
    (notify: "...")
]

Iterations stop on any of: reaching max:, host zone rebuilt, navigation, or render generation superseded.


(watch: $variable)[content]

Re-renders content whenever the named $variable changes. It's not timer-based; it's backed by @preact/signals-core reactive bindings. Use for live displays that update only in response to state change.

ana
// Live health display in a sidebar zone
(watch: $player.health)[
    Health: $player.health / $player.maxHealth
]

// Styled reactive display
(watch: $player.health)[
    (text-style: "bold")[$player.health] / $player.maxHealth
]

The block re-executes from scratch on each change, so all macros inside it ((if:), (text-style:), (text-color:), etc.) are fully evaluated every time.

The binding is registered to the current zone and is automatically cleaned up by zone teardown, with no manual unsubscribe needed.

Watching a single variable is the typical pattern. To watch multiple variables, use multiple (watch:) calls or derive a computed value into a single variable and watch that.

ana
(watch: $player.health)[Health: $player.health]
(watch: $player.gold)[Gold: $player.gold]

(watch-var: $variable)

Displays the current value of a $variable inline and replaces it reactively whenever the variable changes. No block is needed; it renders the variable's value as text.

ana
Gold: (watch-var: $player.gold)
Health: (watch-var: $player.health) / $player.maxHealth
Current location: (watch-var: $world.location)

Use (watch-var:) for plain variable display in a line of prose. Use (watch:)[...] when you need conditional logic, styling, or any other macro inside the reactive content.

SituationUse
Display a variable value inline(watch-var: $player.gold)
Conditional or styled reactive content(watch: $player.health)[(text-color: "red")[$player.health]]
Multiple variables in one reactive block(watch: $score)[Level: $world.level · Score: $score]

Both $variable and $ns.id.key forms are supported:

ana
NPC location: (watch-var: $npc.bartender.location)