Appearance
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-durationsIn 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.timeOfDayis a period name string.linear: Time advances in numeric units (hours).$world.timeOfDayis 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 7Has 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:
| Value | HUD 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.
| Situation | Use |
|---|---|
| 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)