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.

Making Custom Chapter Events

Written 10 Nov 2026

Chapters come with built-in actions like [Pause(1)] and [Music(theme)], but you can also make your own. Any action the Revealer doesn't recognise is sent out as an event with the same name, and your scene script can listen for it and do whatever you like: show a character, flash a light, show a title card, unlock an achievement.

In this tutorial we'll make [ShowGhost()] and [HideGhost()] actions that fade an image in and out while the dialogue plays.

Writing the action

In a chapter node, write the actions wherever you want them to happen, just like built-in ones:

The lights flicker.[ShowGhost()] Did you see that?[AwaitClick()]
It's gone.[HideGhost()]

The name is up to you, but it can't be one the Revealer already uses (see Actions for the list), and it's case-sensitive, so [showghost()] and [ShowGhost()] are different events.

If nothing is listening for an action, the Revealer skips it without any error. So if your event never seems to fire, check the spelling first.

Setting up the scene

In the Scene Editor, open the scene that shows your dialogue and add the image you want to fade in and out.

  1. Follow the Animations section of Element States to give the image a show state that fades it in and a hide state that fades it out.
  2. Give the image the Id ghost, so the script can reach it as this.dom.ghost (see Shared Node Properties).

To do this click on the ID button next to the randomly assigned ID in the inspector image.png

And give it the ID ghost image.png

  1. To keep the image hidden until [ShowGhost()], set its Opacity to 0. The fade-in still brings it up to full opacity.

Listening for the event

Open the scene's script and register a handler for each action with Revealer.Register when the scene starts:

// Called when scene is ready
onStart() {
  Revealer.Register("ShowGhost", this.showGhost, this);
  Revealer.Register("HideGhost", this.hideGhost, this);
},

showGhost(event: RevealerEvent) {
  this.dom.ghost.setState("show");
},

hideGhost(event: RevealerEvent) {
  this.dom.ghost.setState("hide");
},

The this at the end of each Register tells the Revealer which scene the handler belongs to. It does two jobs:

  • Inside the handler, this is your scene, so this.dom.ghost works.
  • The handler is removed automatically when the scene exits. Without that, coming back to this scene would register it a second time and every action would run twice.

To stop listening before the scene exits, call Revealer.Unregister("ShowGhost", this.showGhost).

Press Play (Play & Preview) and run the chapter. When the text reaches [ShowGhost()], the image fades in while the text carries on.

The Chapter Editor's own preview doesn't run scene scripts, so always test custom events with Play.

Making the text wait

If a handler returns a Promise, the Revealer waits for it to finish before revealing the next character. setState returns a Promise that finishes when the state's animation ends, so returning it makes the text pause until the fade is over:

showGhost(event: RevealerEvent) {
  return this.dom.ghost.setState("show");
},

You can also make the handler an async function and await as many things as you like. The text waits until the whole function is done.

To pause the text with no fixed end, call Revealer.Pause() in your handler and Revealer.Unpause() later, for example when the player clicks a button.

Passing arguments

Actions can take arguments, and they arrive on the event object:

  • event.args: the positional arguments, as a list.
  • event.kwargs: named arguments written as name=value.

Rather than writing a pair of actions for every image, you can pass the Id instead. Then [Show(ghost)] and [Hide(ghost)] work for any node with show and hide states:

onStart() {
  Revealer.Register("Show", this.show, this);
  Revealer.Register("Hide", this.hide, this);
},

show(event: RevealerEvent) {
  return this.dom[event.args[0]]?.setState("show");
},

hide(event: RevealerEvent) {
  return this.dom[event.args[0]]?.setState("hide");
},

The ?. means a mistyped Id does nothing instead of causing an error.

Named arguments are handy for options. Here [Show(ghost, wait=no)] lets the text carry on during the fade, while a plain [Show(ghost)] still waits:

show(event: RevealerEvent) {
  const done = this.dom[event.args[0]]?.setState("show");
  if (event.kwargs.wait !== "no") return done;
},

Every value arrives as text, so convert it with Number() when you need a number. To pass text containing commas, wrap it in quotes so it stays as one argument: [ShowTitle("Chapter One, The Return")].

Good to know

  • Skipping: custom events still fire when the player skips the text. If your handler makes the text wait, skipping waits for it too, so keep waits short.
  • More than one listener: you can register several handlers for the same name, and they run one after another in the order they were registered.
  • Newlines: an action followed by a newline swallows it, so you can put each action on its own line. See Actions.
  • Debugging: put a log(event) in your handler to see exactly what arrived. See Debugging.

For the full list of Revealer events and methods, see Revealer and Coding.

Further reading

  • this on MDN: how JavaScript decides what this is inside a function.
  • How to use promises on MDN: a beginner guide to Promises, async and await.
  • Promises, async/await on javascript.info: the same topic, step by step with exercises.