Appearance
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–200Custom 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
ctxbridge, 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:
| Method | Description |
|---|---|
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):
argsis 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 throughctx→ a content macro.
The ctx surface (richer than (script:)'s, since it can emit content and mount DOM):
| Method | Description |
|---|---|
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
GameInitruns. Authorscripts/*.jsload 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)