Skip to content

Declarations, Escape Hatches & System Toggles

Declaring variables and bounds in GameInit, dropping to raw HTML/CSS/JS when the language can’t express something, and enabling or disabling whole engine systems.

Declaration Macros

Unified variable setup. All game variables are declared with (declare:) in GameInit: initial value, bounds, and special system registration in one call.


declare

(declare: $variable, initial)

Plain declaration, no bounds.

ana
(declare: $player.gold, 100)
(declare: $world.events, [])
(declare: $player.name, "")

declare (with bounds)

(declare: $variable, initial, min, max)

Declaration with bounds. Any subsequent (add:) or (sub:) auto-clamps.

ana
(declare: $player.health, 100, 0, 100)
(declare: $player.reputation, 0, -100, 100)

declare (batch namespace)

(declare: $namespace, key, value, key, value, ...)

Batch flat namespace. Declares multiple variables under the same namespace prefix. Equivalent to separate (declare: $ns.key, value) calls.

ana
(declare: $player,
  gold, 100,
  name, "",
  maxHealth, 100,
  events, [])

npc-define

(npc-define: "id", key, value, key, value, ...)

Preferred way to create an NPC. Sets multiple properties on the $npc namespace under the given id. Matches the (item-define:)/(equip-define:)/(status-define:) family: define entities with *-define, declare plain variables with (declare:).

ana
(npc-define: "bartender", name, "Old Bill", gender, "male", age, 58, location, "bar", events, [])

(declare: $npc.id, key, value, ...) still works and is the form to use for custom nested namespaces ((declare: $faction.guild, ...)). For NPCs specifically, prefer (npc-define:).


rel-defaults

(rel-defaults: initial, min, max)

Relationship configuration; see Relationships for full detail. Sets global bounds/initial (and per-dimension defaults). Replaces the old (declare: $rel, ...) config form.

ana
(rel-defaults: 0, -100, 100)

rel-tiers

(rel-tiers: ...)

Sets global or per-NPC relationship tier labels; see Relationships.

ana
(rel-tiers: -50, "hostile", 0, "neutral", 50, "friendly")

declare (relationship)

(declare: $rel.id, initial, min, max)

Per-NPC relationship declaration. Declares the variable and auto-registers it with the relationship system for (tier:) lookups and clamping. (Global config uses (rel-defaults:) above, not (declare:).)

ana
(declare: $rel.bartender, 0, -100, 100)

declare (skill)

(declare: $skill.id, initial)

Skill declaration. Auto-applies the engine's configured skill bounds (default 0–100). Override with explicit min/max.

ana
(declare: $skill.persuasion, 5)            // auto-bounds 0–100
(declare: $skill.crafting, 10, 0, 50)      // explicit override

refine

(refine: $variable, min, N) · (refine: $variable, max, N) · (refine: $variable, min, N, max, N)

Updates the bounds of an already-declared variable. The current value is clamped to the new bounds immediately.

ana
// Level-up increases max health:
(refine: $player.health, max, 150)

// Change both bounds:
(refine: $player.reputation, min, -50, max, 150)

skill-range (GameInit only)

(skill-range: min, max)

Sets the default bounds applied to all (declare: $skill.*, ...) calls that don't specify explicit min/max. Call before any skill declarations.

ana
(skill-range: 0, 200)
(declare: $skill.persuasion, 10)   // auto-bounds 0–200

Computed Variables

Derived values you define once and read like any other variable.

computed-define (GameInit only)

(computed-define: "name", expression)

Defines a reactive computed variable, read as $calc.name. The expression is captured and re-evaluated on demand, so $calc.name always reflects the current state it derives from, with no need to recompute it by hand in every passage. Use it for a value you'd otherwise rewrite everywhere: a modifier, a derived stat, a running total.

Because it's backed by a reactive signal, it updates live: shown inside (watch:) or (watch-var:), $calc.name re-renders the instant any state it reads changes. Read in plain prose, it shows the current value at render time. Use (cond:) for branching inside the expression (it's a value, so use the expression-form conditional, not the (if:) block).

ana
// GameInit: define once
(computed-define: "attractionBruce",
  $rel.bruce.attraction + (cond: (has: $equip.outfit, "sexy"), 2, 0))

// Anywhere: read like a normal variable
Bruce regards you. (Attraction: $calc.attractionBruce)

// Live-updating display
(watch: $calc.attractionBruce)[Bruce's attraction: $calc.attractionBruce]

Notes:

  • Read-only. $calc.name is derived; you can't (set:) it. Change what it reads instead.
  • Not saved. Computed values are recomputed from other state, so they're excluded from save data (and rebuilt when GameInit re-runs on load).
  • Tracks signals, not snapshots. Reactivity follows the $variables (and system mirror signals like $inv.<id>, $rel.<id>, $status.<id>) the expression reads. A value pulled through a macro that bypasses signals (e.g. (count: $inv, "id")) is correct when first read but won't re-trigger updates on its own; reference the $-path you want to react to.

Custom Macros

Define your own macros in-language by composing existing ones.

macro

(macro: "name", params…)[body]

Defines a reusable macro. The body is any passage content: prose, other macros, (if:)/(each:), etc. Each parameter is a bare symbol bound inside the body as a _temp of the same name (param who_who). Define macros in @system(init) so they are registered before any passage uses them (custom macros are not saved; they re-register every new game / load).

Parameters and defaults. Parameters are positional. A literal or expression immediately after a parameter is its default, used when the caller omits that argument:

ana
(macro: "greet", name, mood, "neutral")[
    Hello _name, feeling _mood today.
]
(greet: "Sam", "calm")    // Hello Sam, feeling calm today.
(greet: "Sam")            // Hello Sam, feeling neutral today.

Content macros emit whatever their body renders:

ana
(macro: "healthbar", current, max)[
    (meter: _current, _max)
]
(healthbar: $player.hp, 100)
(healthbar: $rival.hp, 100)

Value macros add a trailing bare return marker. The macro then yields the value of the last expression in its body (its rendered content is suppressed), so it can be used inside (set:), (if:), and other expressions. There is no separate (return:) macro; the marker on the definition is the only thing you need.

The body's last line must be an expression-producing macro call, and that's what the macro hands back. A bare line like _n * 2 is prose, not a value; wrap the computation in a macro ((sum:), (cond:), (first:), …):

ana
(macro: "deck-draw", return)[
    (first: $deck)
]
(set: $hand to (deck-draw:))         // $hand is the first card

(macro: "can-afford", cost, return)[
    (cond: $player.gold >= _cost, true, false)
]
(if: (can-afford: 50))[You can afford the ring.]

Rules:

  • Names follow the usual macro rules: letters, digits, hyphens, underscores; no dots.
  • You cannot override a built-in macro (it is an error). Redefining one of your own custom macros replaces it (last definition wins) with a console warning.
  • If a custom macro throws while running, the error is shown inline and the rest of the passage still renders.
  • For logic the macro language can't express (shuffling a deck, a canvas minigame, custom animation), drop to JavaScript; see Extending with JavaScript.

Escape Hatches

Raw HTML, CSS injection, and scripted JavaScript.

These are advanced features intended for complex custom UI, minigames, or one-off effects that the passage language can't express.

For a full, example-driven walkthrough (the ctx bridge, building dynamic markup, animated canvases, and the timing/cleanup rules), see Extending with JavaScript. The entries below are the quick API reference.

html

(html: "...")

Pushes raw HTML into the current zone. Same rendering path as (meter:).

ana
(html: "<div class='my-box'>Custom markup</div>")

css

(css: "...")

Injects a <style> tag into document.head. Idempotent: calling it twice with the same string does nothing (hash-keyed). Persists across passages.

ana
(css: ".my-box { border: 2px solid gold; padding: 1rem; }")

// Font injection: idempotency means this is safe to call from any passage
(css: "@import url('https://fonts.googleapis.com/css2?family=Lora');")

script

(script: "...")

Runs a JavaScript function body. A ctx object bridges Ana state and a few engine settings:

MethodDescription
ctx.get("ns.key")Read a declared $ variable (no $ sigil)
ctx.set("ns.key", value)Write a declared $ variable (type/bounds enforced, reactive)
ctx.temp("name")Read a _temp variable (no _ sigil)
ctx.rebind("actionId", "key")Rebind a keybind (e.g. ctx.rebind('inventory', 'b'))
ctx.setClockFormat("fmt")Set the HUD clock format
ctx.setDateFormat("fmt")Set the HUD date format

The return value (if an object) has its keys injected as _temp variables in the current passage scope:

ana
(script: "return { roll: Math.floor(Math.random()*20)+1, bonus: 3 }")
// _roll and _bonus are now available as temp vars
You rolled _roll (+ _bonus bonus)!

(script: "return { roll: Math.floor(Math.random() * 20) + 1 }")
You rolled _roll!

(script: "
  const hp = ctx.get('player.health');
  ctx.set('player.health', Math.min(hp + 10, 100));
  return { healed: Math.min(10, 100 - hp) };
")
Healed _healed HP.

Note: (script:) is an advanced escape hatch. Prefer engine macros for anything that has one. Return value keys become temp vars only if the return value is a plain object.

Reusable JS macros: scripts/ & macros.js

Register a named macro backed by JavaScript once and call it like a built-in from any passage. A game author drops any .js file in the project's scripts/ folder; a mod ships a macros.js next to its mod.meta. Either file default-exports a function that receives the engine API and registers macros:

js
// scripts/cards.js  (or  mods/<mod>/macros.js)
export default function register(engine) {
  engine.macros.register('deck-shuffle', (args, ctx) => {
    const deck = Array.isArray(args[0]) ? args[0].slice() : [];
    // …Fisher–Yates…
    return deck;                       // a value macro: usable in (set:)/(if:)
  });
}
ana
(set: $world.deck to (deck-shuffle: $world.deck))

The handler, (args, ctx):

  • args is the call's arguments, already evaluated to plain JS values (variadic by default, so validate them yourself).
  • Return a value → a value macro (usable in (set:)/(if:)). Return nothing and emit through ctx → a content macro.

The ctx surface (richer than (script:)'s, since it can emit content and mount DOM):

MethodDescription
ctx.get("ns.key")Read a declared $ variable
ctx.set("ns.key", value)Write a declared $ variable (type/bounds enforced, reactive)
ctx.temp("name")Read a _temp variable
ctx.setTemp("name", value)Set a _temp visible to the rest of the current passage
ctx.print(text)Emit text into the current zone (joins the prose flow)
ctx.html(markup)Emit a raw HTML block into the current zone
ctx.feedback(category, message)Push a player-facing feedback message
ctx.notify(message)Push a transient notification
ctx.mount()Create & return a <div> mounted into the zone; draw into it (canvas/animation)
ctx.onCleanup(fn)Run fn when the player navigates away (cancel timers/loops)

The engine's render queue, zone teardown, layout, and navigation are deliberately not exposed: a macro can shape its own output and game state but can't destabilise the core.

Loading, collisions & errors:

  • Loaded once at boot, after all built-ins exist and before GameInit runs. Author scripts/*.js load first, then mods in load order. Unlike in-language (macro:), JS macros persist across new-game/load (they are not re-registered per game).
  • Names follow the usual rules (letters, digits, hyphens, underscores; no dots). You cannot override a built-in; registration is refused and logged.
  • Two registrations of the same name → last to load wins, with a console warning.
  • A macro that throws at call time shows an inline error; the rest of the passage still renders.

For a full, example-driven walkthrough (compute, dynamic markup, animated canvas with managed cleanup), see Extending with JavaScript.

System Toggles

Turn off the systems your game doesn't use.

systems-enable

(systems-enable: name) enables a named engine system (the inverse of (systems-disable:)). Call from @system(init). Warns (but still enables) if the system depends on another that is currently off.

systems-disable

(systems-disable: name) disables a named engine system. Call from @system(init). Refused (with a console warning) if another enabled system depends on it; disable the dependent first.

Toggleable systems: achievements, quests, skills, dice, inventory, equipment, containers, status, shops, relationships, currency.

Always on (not toggleable): time (too entangled: the clock, status durations, and scheduling all depend on it; use (time-mode:) for its display) and linkSeen (internal state behind (link-once:), not an author-facing system).

Disabling a system removes its dedicated macros from the registry (e.g. disabling shops removes (shop-buy:), (shop-define:), …). The polymorphic verbs ((add:), (remove:), (has:), (count:), (get:), (ids:), (filter:), (query:), (contains*:)) are shared across systems and with plain variables, so they stay available; a disabled system holds no data for them.

Dependency graph: a system can't be left enabled when something it needs is off:

SystemDepends on
equipmentinventory
containersinventory
shopscurrency, inventory
questsinventory, relationships, currency

Standalone (no dependencies): inventory, relationships, status, currency, dice, achievements, skills.

ana
// Minimal game: turn off everything you're not using.
// Disable dependents before their dependencies.
(systems-disable: quests)
(systems-disable: shops)
(systems-disable: equipment)
(systems-disable: containers)
(systems-disable: inventory)
(systems-disable: dice)

When you disable a system, also stop using its macros and screens (e.g. don't open the Equipment template with equipment off). Disabling marks intent and removes the macros; it does not rewrite your passages.