Skip to content

Time, Scheduling & Reactivity

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

Time

Ana tracks in-game time as $world.timeOfDay and a running day count as $world.days. It fires @on(dayAdvance) when the day rolls over and @on(periodChange) when the named period changes.

Choosing a time model

There are three setups, depending on how much detail your game needs:

Setup(time-mode:)$world.timeOfDay holdsWhat you get
Cyclecycle (default)a period name ("morning")Named periods that advance one step at a time. No clock.
Linearlineara number (hours)A numeric clock. (time-advance:) adds hours; the day rolls over at a fixed length.
Linear + period bandslineara number (hours)A numeric clock that also reports a named period; (time-periods:) durations carve the day into hour-bands.

The one rule to remember about period durations: in cycle mode every (time-advance:) step moves exactly one period and the durations are ignored. In linear mode, the durations in (time-periods:) define how many hours each period band lasts, and their sum is the length of one day.


time-mode

(time-mode: mode)

Sets the time model. Call in GameInit. Accepts only cycle or linear; any other value raises an error.

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

time-periods

(time-periods: name, ...) · (time-periods: name, hours, ...)

Declares the named periods of day, in order. Call in GameInit. Two forms:

Names-only form, for cycle mode. Each (time-advance:) step moves to the next period:

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

Name/duration pairs, for linear mode. Each name is followed by how many hours that period band lasts; the durations sum to the length of a day:

ana
(time-periods: "Dawn", 2, "Day", 8, "Dusk", 2, "Night", 12)
// A 24-hour day: Dawn = hours 0–1, Day = 2–9, Dusk = 10–11, Night = 12–23

In linear mode the current period is then resolved from $world.timeOfDay and reported by (time-now: period).

If (time-periods:) is never called, no periods exist, so (time-now: period) returns an empty string and the #ana-clock period line stays hidden until you declare some.


time-calendar

(time-calendar: "YYYY-MM-DD") · (time-calendar: month, M, day, D, startDay, "Weekday")

Anchors the in-game start date so the engine can compute weekday names, advance the date as days pass, and drive day-of-week scheduling. This is independent of the date format: (time-format: date, ...) controls only how a known date is rendered; the calendar controls what date it is. 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 for the day-of-week computation 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 line still shows when periods or linear mode are configured.


time-advance

(time-advance: amount)

Advances time by amount units (default 1): hours in linear mode, period steps in cycle mode. When the day threshold is crossed, @on(dayAdvance) fires; when the period changes, @on(periodChange) 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 (linear) / 2 periods (cycle)
(time-advance: 8)     // sleep through the night

time-set

(time-set: hour) · (time-set: "period")

Sets the current time directly. Polymorphic by argument type:

  • A number sets the clock hour (linear mode). Setting an hour in cycle mode logs a warning and has no meaningful effect.
  • A string jumps to a named period. This fires the periodChange event, triggering any @on(periodChange) passages and scheduled entries, just as (time-advance:) does when the period changes naturally. In linear mode the period name resolves to the hour where that band begins.
ana
(time-set: 18)         // linear: jump to 6pm
(time-set: "evening")  // cycle (or linear): jump to the "evening" period

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.
]

For computed reads that work regardless of time mode, use (time-now:) below instead of touching $world.timeOfDay directly.


time-now

(time-now: selector) · (time-now: date, format)

Reads the current time. The first argument selects what to return:

SelectorReturns
periodthe current period name (e.g. "morning"). In cycle mode this is what $world.timeOfDay holds; in linear mode it is resolved from the current hour. Empty string when no periods are configured.
hourthe current hour as a number in linear mode (e.g. 9.5 for 9:30). Returns nothing in cycle mode.
datethe formatted current date string, the same text shown on the #ana-clock date line, honoring the display set on (time-calendar:). Empty string when no calendar is configured.

For date, an optional second argument overrides the date format for this call only: "US", "EU", or "ISO" (see time-format). Otherwise the engine's configured format is used.

ana
(if: (time-now: period) is "night")[The streets are quiet.]
(if: (time-now: hour) >= 22)[It's getting late.]

You wake on (time-now: date).
(time-now: date, "ISO")

time-format

(time-format: clock, "24h", date, "EU")

Sets the display formats for the #ana-clock HUD as positional pairs. Either key may be omitted. Does not affect $world.timeOfDay; display only. Also usable in a settings screen to let players override the author defaults.

KeyValuesHUD shows
clock"12h" (default)9:30 AM
clock"24h"09:30
date"US" (default)Saturday, May 23
date"EU"Saturday, 23 May
date"ISO"Saturday, 05-23
ana
(time-format: clock, "24h")
(time-format: date, "EU")
(time-format: clock, "24h", date, "ISO")

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

(schedule: npc, "id", period, "name", location, "value") · (schedule: event, "id", period, "name", trigger, "PassageName")

Registers a period-based schedule entry. The first argument is the type:

  • npc: when the period transitions to name, sets $npc.<id>.location to value.
  • event: when the period becomes name, the named passage executes as an action.
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: 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 (hour ranges)

(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

(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

(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

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

Re-renders content on an interval. The max, N count is an optional comma-pair; when omitted it defaults to 60 iterations, which is also the engine's enforced upper bound.

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

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


watch

(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

(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)