Appearance
Modding Guide
How mods extend an Ana game: directory layout, the mod.meta manifest, load order, and the injection patterns (@into / @wrap / @on) that let a mod add content without editing a single base-game file.
Reference:
$modsnamespace & mod queries. Guide: Hooks & Events teaches@hook/@into/@wrap/@onin depth; Boot, Saves & Mods covers the boot sequence and save/version model.
Overview
Ana mods are collections of .ana passage files (plus optional assets and JavaScript) that load after the base game's passages. Because Ana's hook and wrap system is injection-based, a mod adds, modifies, and extends content without editing any base-game file, and mods use the exact same API the engine's own systems do, with no privileged internal access a mod can't replicate.
What a mod can do:
- Inject links and content into any
@hookthe base game exposes - Wrap any passage to run behavior before/after (or instead of) it
- Register new NPCs, items, containers, and events
- Add new scenes and quests
- Handle engine events (
dayAdvance,gameLoad, …) - Ship a
macros.jsto register named JavaScript-backed macros (deck logic, canvas widgets, minigames); see JavaScript macros
What a mod cannot do:
- Override a base-game passage (passage names must be unique)
- Change the declared type of a
$variable - Override a built-in macro (a JS-macro registration that collides with a built-in is refused)
Directory structure
Place mods under mods/ in the project root:
mods/
my-mod/
mod.meta ← required: metadata (name, version, requires, conflicts)
macros.js ← optional: JavaScript-backed macros
passages/
MyMod_Content.ana ← any .ana files; subdirectories allowed
MyMod_Init.ana
assets/ ← optional: images, audio (reference via relative path)The engine discovers every mod.meta under mods/, resolves load order from each mod's requires, then loads all .ana files from mods in dependency order.
The mod.meta manifest
mod.meta is a plain key: value text file, one entry per line:
name: My Mod
version: 1.0.0
author: Your Name
requires: Base Game
conflicts: Incompatible Mod| Field | Required | Description |
|---|---|---|
name | Yes | Unique mod name (used by (mod:) and the $mods namespace) |
version | Yes | Semver string (e.g. 1.0.0) |
author | No | Author name |
requires | No | Mod names this mod depends on (a required mod always loads first) |
conflicts | No | Mod names this mod cannot coexist with |
There is no
load_orderfield. Mod authors don't set a priority; load order is derived from dependencies (below), so the system stays predictable as more mods are added.
Load order & conflicts
Load order is derived from requires via a dependency sort: a mod that requires another always loads after it, and mods with no dependency relationship load alphabetically by name for stability. Removing the manual load_order field means no two mods can both insist on "loading last."
requires: every listed mod must be present, or boot fails with an error naming the missing dependency. A required mod is guaranteed to load before the mod that needs it.conflicts: if a conflicting mod is also loaded, boot fails with an error. Useconflictswhen your mod would produce undefined behavior alongside another.- Circular dependencies (A requires B, B requires A) are detected and reported as an error.
Hook insertion order: base-game passages load first. Mod injections (via @into) are added after base content, in load order, and append by default; a mod can insert before existing hook content with @into(hook, prepend) (below).
Injecting content: @into
@into(hookName) causes a passage to inject its content wherever the base game's @hook(hookName) marker appears.
The base game defines the hook:
ana
:: BarScene
@zone(options)
@hook(bar_options)
(link: "Leave")[(goto: MainStreet)]A mod injects into it, and no base-game file is touched:
ana
:: MyMod_BarOption @into(bar_options)
(link: "Talk to the stranger")[(goto: MyMod_StrangerConvo)]The injected content is placed at the @hook position, before the content that follows it in the base passage. Multiple mods can inject into the same hook; each adds its content in load order.
Prepend vs. append. By default injections append (added after any earlier injections at the hook). A second argument controls placement:
ana
:: MyMod_FirstOption @into(bar_options, prepend) // before other injections
:: MyMod_LastOption @into(bar_options, append) // explicit defaultAll prepend injections render before all append injections; within each group, load order is preserved. @into is deliberately additive and cannot wipe base content; to replace it entirely, use @wrap (below) with no @wrapped().
Wrapping passages: @wrap
@wrap(PassageName) wraps a named passage, running before and/or after it. @wrapped() marks where the original passage body runs:
ana
:: MyMod_WrapBarScene @wrap(BarScene)
// Runs before BarScene
(if: $mymod.events contains "stranger_arrived")[
@zone(sidebar_npc)
A hooded stranger watches from the corner.
]
@wrapped()
// Runs after BarSceneWhen multiple mods wrap the same passage, the last-loaded wrapper is outermost. Omitting @wrapped() replaces the original passage entirely. This is powerful, and the only way to fully replace base content, so use it deliberately.
Event handlers: @on
Handle engine and game events from a mod without modifying any base-game passage:
ana
:: MyMod_OnDayAdvance @on(dayAdvance)
(if: $mymod.events contains "stranger_arrived")[
(add: $mymod.events, "days_since_arrival")
]Common events: dayAdvance (time crosses midnight), periodChange (time period changes in cycle mode), gameLoad (a save is loaded), gameStart (a new game begins). Many system events also expose context as _temp vars for the handler's duration; see Events & Status for the full list.
Custom events
A mod can trigger its own events, and any other mod (or the base game) can handle them:
ana
// Mod A triggers an event
(trigger: "mymod.stranger_appeared")ana
// Mod B (or the base game) handles it
:: Handler @on(mymod.stranger_appeared)
(notify: "You sense something has changed...")Namespace custom event names (modname.eventname) to avoid collisions.
Mod-aware content: passage-exists & $mods
Use (passage-exists:) so base-game content can reference mod passages that may not be loaded, degrading gracefully when the mod is absent:
ana
(if: (passage-exists: "MyMod_SpecialScene"))[
(link: "Visit the special location")[(goto: MyMod_SpecialScene)]
]For content keyed on the mod itself rather than a specific passage, use (mod:) and (mod-list:):
ana
(if: (mod: "Extra Outfits"))[
You see an extra rack of clothes against the wall.
]
(each: _m in (mod-list:))[Loaded mod: _m]The engine fills the $mods namespace at boot with each mod's author, version, requires, conflicts, passages, wraps, and into data.
Mod init passages
Mods that define NPCs, items, containers, or other game objects use an @system(init) passage. It runs alongside the base game's GameInit on new game / (game-new:), before the first (start:) navigation:
ana
:: MyMod_Init @system(init)
// Mod state
(declare: $mymod.events, [])
// New NPC
(npc-define: "stranger", name, "The Stranger", attitude, 0)
// New items
(item-define: "strange_coin", stackable, type, "currency", value, 100)
// New container
(container-define: "strangers_satchel")Because init passages re-run on every load (before saved values are deserialized over them), keep them to declarations, definitions, and config; put one-time side effects in a (start:) passage or an @on(gameStart) handler instead. See Boot, Saves & Mods for the full rule.
JavaScript macros
For behavior the passage language can't express (a card deck's shuffle, a canvas widget, a minigame), a mod can drop a macros.js next to its mod.meta. It default-exports a function that receives the engine API and registers named macros:
js
// mods/my-mod/macros.js
export default function register(engine) {
engine.macros.register('mymod-dice', (args) => {
const sides = Number(args[0]) || 6;
return Math.floor(Math.random() * sides) + 1;
});
}ana
(if: (mymod-dice: 20) >= 15)[A critical hit!]These load once at boot (after author scripts, in mod load order) and persist across new-game/load, like built-ins. The full API (the (args, ctx) surface, content output, mount()/onCleanup() for animations, and the collision rules) is documented in Extending with JavaScript.
Testing your mod
- Place your mod under
mods/your-mod-name/with amod.metafile. - Run
npm run dev: the engine loads mods automatically at boot. - Check the console: mod-loading logs appear:
[Ana] Loaded mod "your-mod-name" (N passage file(s)). - Use the dev debugger (backtick key) to verify your mod loaded:
- Passages panel: search your passage names to confirm they're registered.
- Hooks panel: navigate to the base passage you inject into and verify your passage appears under "Hook Injections."
- Trace panel: walk through your mod's scenes to verify the flow.
- Check for conflicts: if a hook injection doesn't appear, look for mod-load warnings in the console.