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.

Coding

Written 5 Aug 2026 · Updated 13 Nov 2026

Project Mammut is a HTML5 game engine. Scene scripts are written in TypeScript or JavaScript, and new scenes start in TypeScript. The examples on this page are TypeScript.

Every scene in a project has its own script, made of lifecycle hooks and any custom methods you add. This page is a reference for what's available to write against.

Scene lifecycle

onEnter() {
  // Scene is about to be shown.
},

onStart() {
  // Called after onEnter, once attached scenes are ready. this.dom is populated here.
},

onExit() {
  // Scene is being removed. Clean up timers/listeners here.
},

// Add your own methods too. To run one from a chapter's [ActionName(...)] tag, register it with Revealer.Register (see below).

Inside any of these, and any handler registered with this as its owner, this is the scene instance. this.dom, this.root, this.vars, and any custom methods or properties you've added are all available.

this, the scene instance

  • this.root / this.dom.root: the scene's own root element, which gets the same helpers as any other node (see below).
  • this.dom.<nodeId>: every node marked "referenceable" in the Scene Editor, by id.
  • this.vars: the scene's variables, from its Scene Variables list in the inspector.
  • this.attachedScenes: child scenes attached to this one, populated automatically for "scene" nodes.

DOM node helpers

Attached automatically to every node in this.dom (see _setupDomHelpers in sceneBase.js):

  • hide(), show(), isHidden(), toggleVisibility().hidden(boolean): toggle a shared hidden class, independent of the node's own Visible flag and of each other's internal state.
  • disableEvents(), enableEvents(): block or restore mouse and pointer interaction.
  • setState(name, repeat = true): swap in a named CSS state class. Returns a Promise that resolves when that state's CSS animation finishes, or immediately if it has none:
    await this.dom.card.setState("flip");
    
  • Animator nodes: play(clip), pause(), resume(), stop(), restart(clip).
  • Particle nodes: start(), stop(), pause(), resume(), burst(n).

Revealer: text reveal and markup events

Revealer.Init(this.dom.text_box); // or { clickTarget: this.dom.next_button } to click only that
const ctrl = await Revealer.StartRevealChapter("my_chapter");

Revealer.Register("SetCharacter", (event) => {
  const key = event.args[0];
  const variant = event.kwargs?.variant;
});

// A scene method, with the scene as owner
Revealer.Register("SectionStart", this.onSectionStart, this);
  • Register(eventName, handler): calls handler every time the event fires.
  • Unregister(eventName, handler) removes one handler, and unregisterAll(eventName) removes every handler for that event.
  • Owner: every manager's Register (Revealer, Store, InputManager, AudioManager and Logger) takes an optional owner as its last argument, usually this. The handler is then called with your scene as this, and it's removed automatically when the scene exits, so you don't need an Unregister in onExit. Unregister(eventName, this.onSectionStart) still removes it early, and UnregisterScene(this) removes everything the scene registered with that manager.
  • Every handler receives one event object: event.name, event.args (positional), event.kwargs (named key=value), plus event-specific fields like event.options or event.char.
  • Built-in lifecycle events: NodeStart, NodeEnd, SectionStart, SectionEnd, WaitingForClick, OptionsReady, OnTextCharPlace, SetCharacter, plus any custom [ActionName(...)] tag you use in dialogue.
  • Pause() / Unpause() / IsPaused(): freeze or resume the reveal. Returning a Promise from a handler does this automatically until it resolves.
  • RevealNode(nodeId): jump straight to a node.
  • Full reference: /revealer-reference.html, shipped with the app.

Assets: reading project data

Assets.GetCharacter("alice");   // { key, name, image, variants, ... }
Assets.GetChapter("chapter_01");
Assets.GetImage("bg_forest");
Assets.GetAudio("theme_music");
Assets.GetVariant("alice", "smiling"); // a specific variant object
Assets.Get(key, kind);            // generic lookup
Assets.GetAll(kind);              // every asset of a kind
Assets.Register(name, kind, data); // register something at runtime

Store: persistent variables

Store.Get("playerName", "Alex"); // reads, seeding the default if unset
Store.Set("playerName", "Sam");
Store.Register("playerName", (newVal, oldVal) => { /* react to changes */ });
Store.Unregister("playerName", handler);
Store.Register("playerName", this.onNameChanged, this); // scene as `this`, removed on scene exit
Store.GetCharacterTarget();      // { key, variant } that $char and $variant use in chapters
Store.SetCharacterTarget("alice", "smiling"); // same as {character(alice, variant=smiling)}

{$varName} and {set($var, expr)} in dialogue text read and write through this same store. Character variables that chapters change are kept on Assets.GetCharacter(key).variables and saved with the game (see Logic).

SceneManager

SceneManager.SwitchScene("main_menu");
SceneManager.GetScene("settings_panel")?.togglePanel(); // reach another loaded scene directly
SceneManager.GetCurrentScene();
await SceneManager.LoadScene("settings_panel"); // ensure it's loaded before calling into it
SceneManager.AttachScene(name) / DetachScene(name);

Every scene registers itself globally by filename once loaded, so GetScene can reach any currently-loaded scene, sibling or not.

AudioManager

AudioManager.PlayMusic("theme");      // crossfades
AudioManager.StopMusic(fadeSeconds);
AudioManager.PlayAmbient("rain");
AudioManager.StopAmbient(fadeSeconds);
AudioManager.PlaySFX("click");
AudioManager.PlayVoice("line_042");
AudioManager.SetMasterVolume(0.8);    // also Music/Sfx/Ambient/Voice
AudioManager.GetMasterVolume();
AudioManager.ApplySettings(settings);
AudioManager.SaveSettings() / LoadSettings();
AudioManager.Register("SFXPlayed", handler); // and other audio events

InputManager

InputManager.Register("KeyDown", (e) => { if (e.code === "Space") jump(); });
InputManager.Register("KeyDown", "Space", (e) => jump()); // pre-filtered by the manager
InputManager.Register("GamepadButtonDown", InputManager.GamepadButton.A, (e) => jump());
InputManager.IsKeyDown("ArrowLeft");
InputManager.IsGamepadButtonDown(gamepadIndex, button);

code is the physical key, layout-independent (e.g. "Escape", "Space"); key is the layout-aware character. GamepadButton has named constants (A, B, X, Y, LeftBumper, DPadUp, and so on) for the standard controller mapping.

Application

Application.GetPlatform();       // "tauri" or "web"
Application.Quit();
Application.Restart();
Application.EnterFullscreen() / ExitFullscreen() / ToggleFullscreen() / IsFullscreen();
Application.OpenURL(url);

Fullscreen uses the browser's Fullscreen API on web and the native OS window under Tauri. That means Escape always exiting fullscreen is a web-only browser restriction, not something the desktop build has to work around.

SaveManager

SaveManager.Save(slot);
SaveManager.Load(slot);
SaveManager.Exists(slot);
SaveManager.GetSlotInfo(slot);   // { date, scene }
SaveManager.GetGameSettings() / SetGameSettings(settings) / SaveGameSettings();

Logging

log("hello");
logWarning("careful");
logError("something broke");
Logger.Register((level, args) => { /* pipe to your own UI, etc. */ });
Logger.Unregister(handler);

Debugging

You can learn how to debug your projects here.