Skip to content

Modals, Themes & Audio

Modal overlay screens, the theme system, and audio playback.

Modals render a named passage into a full-viewport overlay, blocking game input while open.


Opens the modal overlay and renders the named passage into the modal zone. It stacks, so you can open a modal from within a modal.

ana
(link: "Open inventory")[(modal-open: "InventoryScreen")]

Closes the top modal on the stack. When the stack is empty, the overlay hides.

ana
(link: "Close")[(modal-close:)]

Stack example:

ana
:: InventoryScreen

@zone(modal)
Choose an item.
(link: "Inspect item")[(modal-open: "ItemDetail")]
(link: "Close")[(modal-close:)]


:: ItemDetail

@zone(modal)
Detailed item view.
(link: "Back")[(modal-close:)]

Theme System

Player-selectable themes backed by CSS html[data-theme="name"] overrides.


(theme-define: "id", "Label")

Registers a theme. Call in GameInit. The engine expects the CSS file at styles/themes/{id}.css, which must be linked in index.html. Authors can define as many themes as they like.

ana
:: GameInit @system(init)

(theme-define: "dark",    "Dark")
(theme-define: "light",   "Light")
(theme-define: "default", "Default")
(theme-define: "dracula", "Dracula")   // custom theme

(theme: "id")

(theme: "id", persist, false)

Switches the active theme by setting the data-theme attribute on <html>. By default, the choice is saved to localStorage and restored on the next session.

Pass persist, false for narrative effects that should not override the player's preferred theme:

ana
// From a settings screen — permanent player choice
(theme: $world.selectedTheme)

// Narrative effect — temporary, reverts on next theme call
(theme: "dracula", persist, false)
// ... a few passages later, restore player's theme:
(theme: (theme-current:))    // won't help — see note below

Note on transient narrative effects: since (theme: "x", persist, false) doesn't save, you need to track the previous theme yourself:

ana
(set: $world.prevTheme to (theme-current:))
(theme: "dracula", persist, false)
// ... dramatic passages ...
(theme: $world.prevTheme)   // restore

(theme-list:)

Expression macro. Returns an array of registered theme IDs in registration order. Use with (dropdown:) or (link-cycle:) in a settings screen.

ana
// In a settings passage:
(dropdown: $world.theme, "Theme", (theme-list:))

(theme-current:)

Expression macro. Returns the currently active theme ID string (reads data-theme from <html>). Returns an empty string "" when no theme has been set, not null or undefined.

ana
Current theme: (theme-current:)

(if: (theme-current:) is "light")[
    (link: "Switch to Dark")[(action: _Theme_Dark)]
]

// Check if any theme is active:
(if: (theme-current:) is "")[
    No theme set yet.
]

Theme CSS format

Create styles/themes/my-theme.css:

css
html[data-theme="my-theme"] {
  --ana-bg:            #1c1510;
  --ana-text:          #e8ddd0;
  --ana-text-muted:    #8a7d68;
  --ana-border:        rgba(255, 255, 255, 0.12);
  --ana-border-subtle: rgba(255, 255, 255, 0.07);
  /* ... override any --ana-* vars you want to change ... */
}

Only override the tokens you need; all others inherit the :root dark defaults from engine.css.

Link the file in index.html after the other theme files, before game.css:

html
<link rel="stylesheet" href="/styles/themes/dark.css">
<link rel="stylesheet" href="/styles/themes/light.css">
<link rel="stylesheet" href="/styles/themes/my-theme.css">
<link rel="stylesheet" href="/styles/game.css">

Audio

(audio: track, channel, behavior?, durationMs?)

Play an audio track on a named channel. The channel arg replaces the old channel: kwarg form.

  • track: filename (.mp3 appended if no extension)
  • channel: bgm, sfx, ambiance, voice, or a custom channel
  • behavior: play, loop, fade-in, fade-out, stop (default play; inherits from (default-transition: audio, ...))
  • durationMs: optional fade duration in ms (default 1000)
ana
(audio: pub_music, bgm, loop)
(audio: door_open, sfx, play)
(audio: combat_theme, bgm, fade-in, 2000)
(audio: old_track, bgm, fade-out)

(audio-crossfade: newTrack, channel, durationMs?)

Fades out the current track on channel, then fades in newTrack. The total transition takes durationMs ms (default 1000; each half takes durationMs / 2).

ana
(audio-crossfade: combat_theme.mp3, bgm, 1500)

(audio-channel: name)

Define a custom audio channel beyond the four built-ins (bgm, sfx, ambiance, voice). Call in GameInit.

ana
(audio-channel: "narrator")