Appearance
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 holds | What you get |
|---|---|---|---|
| Cycle | cycle (default) | a period name ("morning") | Named periods that advance one step at a time. No clock. |
| Linear | linear | a number (hours) | A numeric clock. (time-advance:) adds hours; the day rolls over at a fixed length. |
| Linear + period bands | linear | a 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.timeOfDayis a period name string.linear: time advances in numeric hours.$world.timeOfDayis 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–23In 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:
| 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 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 nighttime-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
periodChangeevent, 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" periodReading 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:
| Selector | Returns |
|---|---|
period | the 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. |
hour | the current hour as a number in linear mode (e.g. 9.5 for 9:30). Returns nothing in cycle mode. |
date | the 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.
| Key | Values | HUD 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 toname, sets$npc.<id>.locationtovalue.event: when the period becomesname, 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.
| 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)