LiveGame
LiveGame is the main class that represents the game's current state.
Public Properties
game
The Game instance
story
The current Story instance
Public Methods
getStorable
Returns the Storable instance
The same instance for the life of the LiveGame — newGame() and deserialize() rebuild the namespaces inside it, not the store itself, which is why a subscription made through Watching stored values survives both.
return: Storable- Storable
get storable
newGame
Starts a new game
return this
deserialize
Load a saved game
After calling this method, the current game state will be lost, and the stage will trigger force reset
**Note: **Even if you change just a single line of script, the saved game might not be compatible with the new version
Example:
const savedGame = {
// ...saved game data
};
// use hook inside a component
const {game} = useGame();
// pass the saved game data to the game instance
game.getLiveGame().deserialize(savedGame);savedGame: SavedGame- SavedGame
serialize
Serialize the current game state
You can use this to save the game state to a file or a database
**Note: **Even if you change just a single line of script, the saved game might not be compatible with the new version
return: SavedGame- See SavedGame`
onCharacterPrompt
Called when a character says something
fc: (event: LiveGameEvent["event:character.prompt"]) => void- See LiveGameEvent- Returns
LiveGameEventToken- See LiveGameEventToken
const {game} = useGame();
const [texts, setTexts] = useState<string[]>([]);
useEffect(() => {
const token = game.getLiveGame().onCharacterPrompt((event) => {
setTexts((prevTexts) => [...prevTexts, event.text]);
});
return () => {
token.cancel();
};
}, []);
return (
<div>
{/* Your Text Log */}
</div>
);onMenuChoose
Called when a menu is completed
fc: (event: LiveGameEvent["event:menu.choose"]) => void- See LiveGameEvent- Returns
LiveGameEventToken- See LiveGameEventToken
capturePng
Capture the game screenshot, will only include the player element
Returns a PNG image base64-encoded data URL
**Note: **Image returned by this method is not compressed, and it is not affected by the
screenshotQualityoption
const {game} = useGame();
function handleButtonClick() {
game.getLiveGame().capturePng().then((dataUrl) => {
// do something with the dataUrl
});
}- Returns
Promise<string>
captureJpeg
Capture the game screenshot, will only include the player element
Returns compressed JPEG image data URL
- Returns
Promise<string>
captureSvg
Capture the game screenshot, will only include the player element
Returns an SVG data URL
- Returns
Promise<string>
capturePngBlob
Capture the game screenshot, will only include the player element
Returns a PNG image blob
- Returns
Promise<Blob | null>
requestFullScreen
Request full screen on Chrome/Safari/Firefox/IE/Edge/Opera, the player element will be full screen
Note: this method should be called in response to a user gesture (for example, a click event)
Safari iOS and Webview iOS aren't supported, for more information, see MDN-requestFullscreen
options?: FullscreenOptions | undefined- Returns
Promise<void> | void
exitFullScreen
Exit full screen
- Returns
Promise<void> | void
onPlayerEvent
Listen to the events of the player element
const {game} = useGame();
useEffect(() => {
return game.getLiveGame().onPlayerEvent("click", (event) => {
// do something
}).cancel;
}, []);type: K- The event typelistener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any- The event listeneroptions?: boolean | AddEventListenerOptions- Returns
LiveGameEventToken
getHistory
Get the game history. This method is used to create backlog.
Since save format v2 the backlog is persisted, so this returns the full history immediately after deserialize — a loaded game no longer starts with an empty backlog.
import { useGame, GameHistory } from "narraleaf-react";const game = useGame();
const history = game.getHistory();
function handleUndo(history: GameHistory) {
game.undo(history.token);
}
return (
<div>
<h3>Backlog</h3>
{history.map((item) => (
<div
key={item.token}
onClick={() => handleUndo(item)}
>
{/* show the action text */}
{/* text is available when the action is "say" or "menu" */}
{item.element.text}
</div>
))}
</div>
);- Returns
GameHistory[]- The game history, see GameHistory
undo
Undo the game action.
game.undo(history.token);token?: string- The token of the history item, see GameHistory. If not provided, the last action will be undone.
Note:
undowalks the in-memory undo history, which only exists while the game runs. After loading a save it has nothing to walk — use restoreToHistory to go back to a line from a loaded game.
restoreToHistory
Restore the game to a past line in the backlog.
Unlike undo, this works after loading a save: every backlog entry carries a self-contained snapshot, so restoring re-applies that snapshot and trims the backlog back to that line, rather than relying on the in-memory undo history.
const history = game.getHistory();
// Jump back to a past line — including one restored from a loaded save.
game.getLiveGame().restoreToHistory(history[0].token);token: string- The token of the history item to restore to, see GameHistory.- Returns
boolean-trueif the line was restored;falseif the token is unknown or the entry has no restore snapshot.
notify
Create a notification.
The style of the notification is defined by Notification.
// notify for 3 seconds
game.notify("Save success", 3000);// to control the notification, set the duration to `null` to make the notification stay forever
const token = game.notify("Fast forward", null);
// cancel the notification when the player releases the right key
window.addEventListener("keyup", (event) => {
if (event.key === "ArrowRight") {
token.cancel();
}
});message: string- The message to notifyduration?: number | null- The duration of the notification, default is 3000ms. Set tonullto make the notification stay forever.- Returns
NotificationToken- See NotificationToken
playSound
Play a sound immediately and return its SoundToken.
const {game} = useGame();
game.getLiveGame()
.playSound("https://example.com/voice.mp3")
.then((token) => {
token.once("ended", () => {
console.log("Voice playback completed");
});
});The clip starts at the volume its Sound was configured with — Sound.voice({src, volume: 0.4})
starts at 0.4, not at full volume. A source given as a string or URL becomes a default Sound,
which is full volume; pass a Sound to say otherwise.
Before 0.22.0, "no volume said" was read as full volume here, so a configured volume was
discarded. A clip replayed after setVolume now comes back at the volume it was last set to
instead of jumping to full.
There is no fade: the token's volume is already settled when this resolves and no ramp is left
running, so a setVolume or a fade driven on the returned token afterwards wins outright.
sound: Sound | string | URL- The sound instance, sound source string, or URL- Returns
Promise<SoundToken>- See@NarraLeaf/Soundfor more information about the soundToken instance
waitForRouterExit
Wait for the router to exit.
This method is useful when you want to create a new game and wait for the router to exit.
const {game} = useGame();
const router = useRouter();
const liveGame = game.getLiveGame();
useEffect(() => {
router.clear().cleanHistory();
const token = liveGame
.newGame()
.waitForRouterExit()
token
.promise
.then(() => {
dispatchState({ isPlaying: true });
});
return () => {
token.cancel();
};
}, []);- Returns
{ promise: Promise<void>; cancel: VoidFunction; }
waitForPageMount
Wait for the page to mount
const {game} = useGame();
const router = useRouter();
const liveGame = game.getLiveGame();
useEffect(() => {
router.push("home");
const token = liveGame.waitForPageMount();
token.promise.then(() => {
// do something
});
return () => {
token.cancel();
};
}, []);- Returns
{ promise: Promise<void>; cancel: VoidFunction; }
onWindowEvent
Listen to the events of the window
const {game} = useGame();
useEffect(() => {
return game.getLiveGame().onWindowEvent("resize", (event) => {
// handle window resize
}).cancel;
}, []);type: K- The event typelistener: (this: Window, ev: WindowEventMap[K]) => any- The event listeneroptions?: boolean | AddEventListenerOptions- Returns
LiveGameEventToken- See LiveGameEventToken
reset
Reset the game state
Note: Calling this method will lose the current game state
const {game} = useGame();
const router = useRouter();
game.getLiveGame().reset();
router.clear().cleanHistory().push("home");skipDialog
Skip the current dialog
game.getLiveGame().skipDialog();fastForward
Fast-forward playback to the next menu, to the end of the story, or to a specific action.
Every line in between is executed for real, so the backlog and its restore snapshots accumulate exactly as in normal play — only faster and silent. Audio is muted for the duration, and the timed pauses the run executes (Control.sleep, auto-forward) resolve at once. It stops as soon as a menu is waiting for a choice, so the choice itself is always left to the player. Because history accumulates the whole way, getHistory and restoreToHistory cover the fast-forwarded span just like normal play.
Skipping a line is a request broadcast to the renderer, not a synchronous state change, so it is re-issued until the line settles. A line that never answers ends the run with "stalled" rather than hanging — this method always settles.
// Jump ahead to the next decision point.
await game.getLiveGame().fastForward();
// Or run to the end of the story.
await game.getLiveGame().fastForward({ until: "end" });
// Or park the play head on a specific action, without running it.
const result = await game.getLiveGame().fastForward({ until: { actionId: "act-42" } });
if (result.reason === "action") {
// parked on act-42, not yet executed
} else if (result.reachedTarget === false) {
// a menu blocked the path, the stack drained, maxSteps was hit, or a line stalled
}options.until?: "menu" | "end" | { actionId: string }-"menu"(default) stops at the next menu;"end"runs until the story finishes;{ actionId }runs until that action surfaces as the next thing to execute and stops just before running it, so the play head is left parked on that line. Only the root execution stack is scanned — an id buried inside an in-flightControl.all/Control.anyor async branch is not a stop point. A menu that blocks the path stops anactionIdrun too, since the target cannot be reached until the player decides.options.maxSteps?: number- safety bound on the number of advance steps (defaults to themaxStackModelLoopconfig).options.stepTimeout?: number- how long, in milliseconds, a single suspended line is given to settle before the run reports"stalled". Default10000. Raise it for a story that fast-forwards through long unskippable media.- Returns
Promise<{ reason: "menu" | "end" | "maxSteps" | "action" | "stalled"; reachedTarget?: boolean }>- why it stopped.reason-"action"reacheduntil.actionId;"menu"a menu is waiting for a choice;"end"the stack drained;"maxSteps"the step cap was hit;"stalled"a line refused to settle withinstepTimeout.reachedTarget- present only whenuntil: { actionId }was requested, andtrueonly for reason"action". A"menu"/"end"run keeps its plain{ reason }shape.
until: { actionId }, the "action" reason and reachedTarget are available since 0.16.0. options.stepTimeout and the "stalled" reason are available since 0.17.1.
A run can end early on a step that cannot be skipped. A step already in flight when the run started, a video (allowSkipVideo is false by default), and a camera or layer transition all ignore the skip request. Such a step still settles on its own if it finishes within stepTimeout, so only one that outlives the timeout ends the run — with "stalled", and with volume and the fast-forward flag restored on the way out.
Before 0.17.1 the run parked on such a step instead: the promise settled neither way, the game stayed muted and permanently in fast-forward mode. Nothing that worked before starts failing, but a host that treats any non-"menu" reason as success should now distinguish "stalled", and an exhaustive switch over reason needs the extra arm to keep compiling.