Skip to content

Time & Scheduling

Game time and reactivity, scheduling future events, and calendar/clock display.

Reference: Time, Scheduling & Reactivity for exact macro syntax.

Timing & Reactivity

Ana provides three bounded timing/reactivity primitives. All three auto-cancel when the host zone is rebuilt or the player navigates away, with no manual cleanup needed.

When to use each

MacroUse for
(after:)One-shot delayed reveal: a figure steps in after a pause, a door slowly opens
(every:)Counted repeating events: a clock ticking N times, looping sound effect triggers
(watch:)Reactive block: conditional text, styled output, or combining multiple values
(watch-var:)Plain variable display inline: show the current value and keep it updated

(after:): delayed reveal

ana
The door creaks open.

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

The content is appended to the current zone after the delay. If the player navigates away before the timer fires, the content never appears.

(every:): bounded repeating events

ana
(every: 1s, max: 3)[
    (notify: "Tick.")
]

max: is required. There is no way to create an infinite timer. If you need something that runs until navigation, use max: 60 (the engine enforces an upper bound anyway).

(watch-var:): inline reactive variable display

The simplest reactive primitive. Displays the variable's current value inline and replaces it whenever the variable changes. Use it anywhere you'd write a $variable but want the display to stay live:

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

No block is needed. It works in any zone: prose, sidebar, header.

(watch:): reactive block

Use (watch:) when the reactive content is more than a bare variable value: conditional text, styling, multiple variables combined, or any macro:

ana
@zone(sidebar_character)

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

// Conditional color
(watch: $player.health)[
    (if: $player.health < 25)[
        (text-color: "crimson")[$player.health]
    ](else:)[
        $player.health
    ]
]

The block re-executes fully on each change, so (if:), (text-style:), and all other macros work correctly inside.

The binding is registered to the zone. When the zone is cleared (on goto, or when the sidebar is rebuilt), the binding is automatically cleaned up.

(watch:) takes a single $variable. If you need to react to multiple variables, use multiple (watch:) calls, or track them through a single derived $ variable and watch that.

Auto-cancellation rules

All three macros share the same cancellation guarantee:

  • Navigation with (goto:) or (update:) increments the render generation; any pending timer or watcher from the old generation silently no-ops
  • Zone teardown (e.g., when (layout:) rebuilds the grid, or when the same zone is cleared by a new render) runs all registered cleanup functions, unsubscribing (watch:) effects and clearing (after:)/(every:) timers
  • You never need to manually stop a timer or unsubscribe a watcher

Scheduling

The scheduling system lets you define rules that fire automatically based on time periods or clock hours. It replaces scattered (if: $world.timeOfDay is ...) checks in every passage with a centralized registry.

Macro signature

ana
(schedule: type, "id", matchKey, matchValue, effectKey, effectValue)
(schedule: type, "id", matchKey, matchValue, effectKey, effectValue, condition, <expr>)
(schedule-clear: type, "id")
  • type: npc or event
  • matchKey: period (named period) or hours (hour range in linear mode)
  • effectKey: location (npc only) or trigger (passage name to execute)

NPC movement by period

Register in GameInit; entries fire automatically when (time-advance:) or (time-set-period:) transitions the world into that period:

ana
:: GameInit @system(init)

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

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

Now (filter: $npc.location is "bar") always reflects the bartender's current location without any per-passage checks.

Conditional entries

Add a condition pair; the entry only fires if 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)

World events via passage triggers

For anything beyond setting a property, fire a trigger passage:

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

:: FoodTruck_Arrive

(set: $world.foodTruckActive to true)
(set: $npc.tacoGuy.location to "main_street")
(notify: "The taco truck is open.")

:: FoodTruck_Leave

(set: $world.foodTruckActive to false)
(set: $npc.tacoGuy.location to "home")

Hour-based scheduling (linear mode)

When the game uses (time-mode: "linear") and advances time numerically, entries can be bound to hour ranges. The effect fires when world.timeOfDay first crosses the range's start hour.

ana
// Full workday schedule — three overlapping-free 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")

[9,11] means the effect fires at hour 9 and the NPC is understood to be there through hour 11. Outside all scheduled ranges the NPC simply has no assigned location.

The engine detects overlapping hour ranges at registration time and throws an error if two entries for the same group overlap (e.g., [9,12] and [10,15]). Non-overlapping adjacent ranges like [9,11] and [12,12] are fine.

Modifying the schedule at runtime

Use (schedule-clear:) after a story event to remove all entries for a group:

ana
// The bar burns down — clear its schedule entries permanently
:: Bar_Burns

(schedule-clear: event, "bar_karaoke")
(schedule-clear: event, "food_truck")
(set: $world.barDestroyed to true)
(notify: "The bar is gone.")

Then re-register new entries if needed:

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

Day-of-week scheduling

Once a calendar is configured (see section 19), schedule entries can target specific weekdays.

day match: fires once each time the named day arrives (at day rollover):

ana
// Taco truck on Main Street every Tuesday
(schedule: npc, "taco_truck", day, "Tuesday", location, "main_street")

// Market opens every Saturday — passage handles setup logic
(schedule: event, "market", day, "Saturday", trigger, "MarketOpen")

day qualifier: restrict a period entry to specific weekdays:

ana
// Only park for lunch on Tuesday and Thursday
(schedule: npc, "taco_truck", period, "noon", location, "main_street",
    day, ["Tuesday","Thursday"])

Without a configured calendar, day qualifiers are silently ignored; entries fire on every matching period instead. This keeps saves forward-compatible if a calendar is added later.

Responding with @on(periodChange)

Any passage tagged @on(periodChange) runs after scheduled entries have fired on every period transition:

ana
:: World_OnPeriodChange @on(periodChange)

(if: $world.timeOfDay is "night")[
    (set: $world.shopOpen to false)
]
(if: $world.timeOfDay is "morning")[
    (set: $world.shopOpen to true)
]

Avoid navigation in @on(periodChange) passages: do not call (goto:) from within an @on handler.

Calendar and Clock Display

The engine injects a persistent #ana-clock element into the game header that shows the current date and time. It appears automatically once any time system is configured, with no layout changes required.

Configuring the calendar

Call (time-calendar:) in GameInit to enable day-of-week tracking:

ana
:: GameInit @system(init)

// Option A — ISO date: engine infers the weekday automatically
(time-calendar: "2026-05-23")

// Option B — partial date: you name the starting weekday
(time-calendar: month, 5, day, 23, startDay, "Tuesday")

The year is stored internally for day-of-week computation but is never shown in the HUD. If you omit the year (Option B), the month and day are used for display only.

Display style

Choose what the date line shows via the display keyword:

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

Clock and date format

Set author defaults in GameInit. Players can override these in a settings screen:

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

These macros affect only the display. $world.timeOfDay always holds the raw period name or numeric hour.

The #ana-clock HUD element

The clock is hidden when no time system is configured. It becomes visible as soon as any of the following are true:

  • (time-calendar:) has been called
  • (time-periods:) has been called
  • (time-mode: "linear") is active

The date line shows only when a calendar is configured. The time line shows whenever periods or linear mode is active. Both lines react to $world.days and $world.timeOfDay signal changes, with no manual refresh needed.