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: $variable, initial)

Plain declaration, no bounds.

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

(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: $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: "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: initial, min, max) / (rel-tiers: ...)

Relationship configuration macros; see Relationships for full detail. (rel-defaults:) sets global bounds/initial (and per-dimension defaults); (rel-tiers:) sets global or per-NPC tier labels. These replace the old (declare: $rel, ...) config forms.

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

(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.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: $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: min, max) (GameInit only)

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

Custom Macros

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

(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:), (arr-first:), …):

ana
(macro: "deck-draw", return)[
    (arr-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: "...")

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

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

(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: "...")

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

Author-intent flags for optional systems.

(systems-enable: name) / (systems-disable: name)

Enable or disable a named engine system. Call from @system(init).

Recognized names: achievements, quests, skills, dice.

ana
(systems-disable: dice)
(systems-disable: quests)

Systems are always instantiated; disabling marks the author's intent and no-ops the system's macros gracefully. Use this to keep a project's GameInit legible about which features are active.

ana
// Minimal game — turn off everything you're not using
(systems-disable: dice)
(systems-disable: quests)
(systems-disable: achievements)