Project Mammut Wiki
Project Mammut is currently in open beta. The UI is being updated a lot during this time, and you may run into bugs. If you'd like to help develop the engine or suggest features, come find us on Bluesky or Discord.

Revealer

Written 5 Aug 2026 · Updated 8 Nov 2026 · Requires Mammut 0.6.2+

Mammut's Revealer is the dialogue engine: it turns a chapter node's into the letter-by-letter reveal players see on screen. It's the main way your own scene scripts find out what is happening during a conversation. This page covers how it actually works. For the full markup syntax and API method list, see the Revealer reference and Coding; for the individual [Action(...)] tags, see Actions and Logic.

The Reveal pipeline

Initialisation

Revealer.Init(element) Initialises the Revealer on the text element that text is revealed into. It also adds a full-screen click layer inside that element, which picks up the player's clicks. Pass { clickTarget: element } to use your own element for clicks instead. See Clicking and skipping below.

Start Chapter

Calling Revealer.StartRevealChapter(...) or Revealer.RevealNode(id) runs a node's content through a fixed pipeline:

  1. The raw text is tokenised into tags, actions, logic blocks, words, and whitespace.
  2. Logic blocks ({if}, {for}, {set}, {print}, {$var}) are evaluated and spliced into plain output tokens.
  3. What remains is revealed token by token: words are typed out character by character at the configured speed, style tags open and close nested spans, and actions run as they're reached.

Only one reveal is ever active at a time. Starting a new node cancels whatever is currently running, so you never need to stop one manually before starting another.

When a node get picked up by the revealer, the first thing that happens is a logic pass is applied to the node's contents. This process will resolve all the logic steps (changing variables, looping resolving if statements etc.), The output is then passed to a second module for revealing. This means if you change variables in a node, this all happens when the node gets loaded regardless of where the variable change instruction is listed in the code.

Built-in actions versus custom actions

Some [Action(...)] tags are handled directly by Revealer: AwaitClick, ClickClear, AwaitClickClear, Clear, Pause, TextSpeed, AutoLink, SwitchScene, LinkOption, and the audio actions (Music, StopMusic, Ambient, StopAmbient, SFX, Voice). Everything else dispatches as an event with the same name, for a scene script to catch with Revealer.Register. See Actions for what each one does.

SetCharacter, UnsetCharacter, and UnsetCharacterAll sit in between: Revealer has no built-in idea of what a character portrait is, so it doesn't act on them directly, but it does pause the reveal automatically the moment one of these fires. That gives your scene script a beat to swap a portrait or play a transition before calling Revealer.Unpause() to let the next line continue.

Handlers and the event object

Every Revealer.Register handler receives one event object: event.name, event.args (positional), and event.kwargs (named). A few built-in events add extra fields, such as options on OptionsReady and char on OnTextCharPlace.

If a handler returns a Promise, the reveal waits for it to resolve before continuing. This is a general mechanism, not something special to character events: any handler for any event can pause the reveal this way simply by being asynchronous.

Control objects versus global control

StartRevealChapter* and RevealNode return a control object scoped to that specific reveal: skip(), pause(), resume(), stop(), isPaused(), and a promise that resolves once the node has fully finished, including any NodeEnd and OptionsReady events it fires.

Revealer.Pause(), Revealer.Unpause(), and Revealer.IsPaused() do the same job globally, without needing a reference to a particular control object. This is what SetCharacter and friends use internally, and it's the more convenient option any time you don't already have the control object for the reveal you want to affect.

Clicking and skipping

The revealer will manage waiting for different types of clicks when needed:

  1. If the click landed on an inline link (a <link> style tag), it jumps straight to that node, the same as [AutoLink(...)].
  2. Otherwise, if the reveal is waiting on [AwaitClick()] or similar, the click resolves it.
  3. Otherwise, the click skips the rest of the current word's typewriter animation, snapping any pending text into place immediately.

An inline <link> tag is a separate mechanism from [LinkOption(...)]: it jumps immediately on click with no OptionsReady step, while LinkOption collects choices and fires them all together once the node ends.

The click layer

By default, Init adds an invisible layer inside the text element that covers the whole game screen, so a click anywhere counts. The layer is only there while a node is revealing. It's hidden:

  • once the node ends, so LinkOption buttons and the rest of your UI can be clicked.
  • while the Revealer is paused, for example after [SetCharacter(...)] or Revealer.Pause(), so UI shown during a pause can be clicked.

The layer sits behind the text, so clicking the text works the same as clicking anywhere else, and inline <link> text always jumps to its node. The layer covers everything drawn below the text box, but anything drawn above the text box (with a higher Z-index, or later in the scene tree) stays clickable.

To keep the layer behind the text, the text element is given isolation: isolate. This means a mix-blend-mode on text inside the text box only blends with the text box, not with what's behind it.

If the text box or one of its parents is rotated, scaled or skewed and also has its overflow set to hidden or scroll, the layer is cut off at that element's edges and won't cover the whole screen.

To have clicks come from your own element instead, such as a "Next" button, pass it as clickTarget:

Revealer.Init(this.dom.text_box, { clickTarget: this.dom.next_button });

A clickTarget is never hidden; clicking it does nothing between nodes or while paused. Inline links in the text still work. To turn click handling off entirely, including links, pass { autoClick: false }.

Expressions

Logic blocks evaluate expressions after substituting $variable references, restricted to a safe set of characters. This is enough for the arithmetic, comparisons, and string handling {if}, {for}, {set}, and {print}