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
(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 overriderefine
(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–200Computed 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.nameis 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
GameInitre-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
ctxbridge, 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:
| 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
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:
| System | Depends on |
|---|---|
equipment | inventory |
containers | inventory |
shops | currency, inventory |
quests | inventory, 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
equipmentoff). Disabling marks intent and removes the macros; it does not rewrite your passages.