Kagari
Type to search documentation.

Game System (E6xxx)

The System built by main() defines the game’s modes: root pages, overlays, dialogs, the game mode and its overlays. Kagari builds a graph of how the nav::* handlers move between these modes. It checks that every mode can be reached, that every mode can be left, and that the mode stack stays bounded.

These checks need a whole game, so each snippet below is a complete program: the same minimal main() with its settings, an entry scene and a game view, followed by the modes that trigger the diagnostic.

W6001 unreachable-mode

Warning. No reachable mode ever opens this mode, so the player can never see it. The warning also fires when no reachable mode calls nav::start or nav::load, so the game itself never starts.

kix
use std::narrative::text::text_style;
use std::screen::size;
static TEXT_SPEED: Setting<Duration> = setting(ms(30));
static AUTO_DELAY: Setting<Duration> = setting(ms(1300));
static AUTO_DELAY_PER_CHAR: Setting<Duration> = setting(ms(60));
static SKIP_UNREAD: Setting<bool> = setting(false);
static MASTER: Setting<Volume> = setting(volume(100));
static MUSIC: Setting<Volume> = setting(volume(60));
static SOUND: Setting<Volume> = setting(volume(100));
static VOICE: Setting<Volume> = setting(volume(100));
static VOICE_SUSTAIN: Setting<bool> = setting(false);
static WAIT_FOR_VOICE: Setting<bool> = setting(true);
static WINDOW_OPACITY: Setting<Int<0, 100>> = setting(80);
static FULLSCREEN: Setting<bool> = setting(false);
static ANIMATIONS: Setting<bool> = setting(true);
const FONT: FontAsset = asset("fonts/default.otf");
fn main() -> System {
System {
title: "Example",
resolution: size(1280, 720),
start: Title,
game: GameView,
save_slots: 20,
auto_slots: 3,
backlog: 200,
font: FONT,
text: text_style(26, WHITE),
settings: Settings {
text_speed: TEXT_SPEED,
auto_delay: AUTO_DELAY,
auto_delay_per_char: AUTO_DELAY_PER_CHAR,
skip_unread: SKIP_UNREAD,
master: MASTER,
music: MUSIC,
sound: SOUND,
voice: VOICE,
voice_sustain: VOICE_SUSTAIN,
wait_for_voice: WAIT_FOR_VOICE,
window_opacity: WINDOW_OPACITY,
fullscreen: FULLSCREEN,
animations: ANIMATIONS,
},
bindings: Vec::new(),
}
}
#[scene]
#[entry]
pushdown fn Start() {}
fn GameView(game: Narrative) -> Element {
game_screen!(game = game)
}
fn Title() -> RootPage {
RootPage(button!{on_click = || nav::start(Start, (), |_end| ()), t!["Start"]})
}
fn Unused() -> Overlay {
Overlay(button!{on_click = || nav::back(), t!["Back"]})
}

Fix: open the mode from a reachable handler (e.g. button!{on_click = || nav::call(Unused), t!["More"]}), or delete it.

E6002 back-from-base

Error. nav::back() runs in a mode at the base of the mode stack, such as a root page or the game mode. Nothing is under it, so going back does nothing.

kix
use std::narrative::text::text_style;
use std::screen::size;
static TEXT_SPEED: Setting<Duration> = setting(ms(30));
static AUTO_DELAY: Setting<Duration> = setting(ms(1300));
static AUTO_DELAY_PER_CHAR: Setting<Duration> = setting(ms(60));
static SKIP_UNREAD: Setting<bool> = setting(false);
static MASTER: Setting<Volume> = setting(volume(100));
static MUSIC: Setting<Volume> = setting(volume(60));
static SOUND: Setting<Volume> = setting(volume(100));
static VOICE: Setting<Volume> = setting(volume(100));
static VOICE_SUSTAIN: Setting<bool> = setting(false);
static WAIT_FOR_VOICE: Setting<bool> = setting(true);
static WINDOW_OPACITY: Setting<Int<0, 100>> = setting(80);
static FULLSCREEN: Setting<bool> = setting(false);
static ANIMATIONS: Setting<bool> = setting(true);
const FONT: FontAsset = asset("fonts/default.otf");
fn main() -> System {
System {
title: "Example",
resolution: size(1280, 720),
start: Title,
game: GameView,
save_slots: 20,
auto_slots: 3,
backlog: 200,
font: FONT,
text: text_style(26, WHITE),
settings: Settings {
text_speed: TEXT_SPEED,
auto_delay: AUTO_DELAY,
auto_delay_per_char: AUTO_DELAY_PER_CHAR,
skip_unread: SKIP_UNREAD,
master: MASTER,
music: MUSIC,
sound: SOUND,
voice: VOICE,
voice_sustain: VOICE_SUSTAIN,
wait_for_voice: WAIT_FOR_VOICE,
window_opacity: WINDOW_OPACITY,
fullscreen: FULLSCREEN,
animations: ANIMATIONS,
},
bindings: Vec::new(),
}
}
#[scene]
#[entry]
pushdown fn Start() {}
fn GameView(game: Narrative) -> Element {
game_screen!(game = game)
}
fn Title() -> RootPage {
RootPage(column![
button!{on_click = || nav::start(Start, (), |_end| ()), t!["Start"]},
button!{on_click = || nav::back(), t!["Back"]},
])
}

Fix: from a base mode, use nav::goto(...) to switch pages, or nav::quit(), instead of nav::back().

E6003 trapped-mode

Error. None of this mode’s handlers calls nav::back, nav::goto, nav::start, nav::load or nav::quit, and no key binding performs Action::Back. Once the mode opens, the player can’t leave it.

kix
use std::narrative::text::text_style;
use std::screen::size;
static TEXT_SPEED: Setting<Duration> = setting(ms(30));
static AUTO_DELAY: Setting<Duration> = setting(ms(1300));
static AUTO_DELAY_PER_CHAR: Setting<Duration> = setting(ms(60));
static SKIP_UNREAD: Setting<bool> = setting(false);
static MASTER: Setting<Volume> = setting(volume(100));
static MUSIC: Setting<Volume> = setting(volume(60));
static SOUND: Setting<Volume> = setting(volume(100));
static VOICE: Setting<Volume> = setting(volume(100));
static VOICE_SUSTAIN: Setting<bool> = setting(false);
static WAIT_FOR_VOICE: Setting<bool> = setting(true);
static WINDOW_OPACITY: Setting<Int<0, 100>> = setting(80);
static FULLSCREEN: Setting<bool> = setting(false);
static ANIMATIONS: Setting<bool> = setting(true);
const FONT: FontAsset = asset("fonts/default.otf");
fn main() -> System {
System {
title: "Example",
resolution: size(1280, 720),
start: Title,
game: GameView,
save_slots: 20,
auto_slots: 3,
backlog: 200,
font: FONT,
text: text_style(26, WHITE),
settings: Settings {
text_speed: TEXT_SPEED,
auto_delay: AUTO_DELAY,
auto_delay_per_char: AUTO_DELAY_PER_CHAR,
skip_unread: SKIP_UNREAD,
master: MASTER,
music: MUSIC,
sound: SOUND,
voice: VOICE,
voice_sustain: VOICE_SUSTAIN,
wait_for_voice: WAIT_FOR_VOICE,
window_opacity: WINDOW_OPACITY,
fullscreen: FULLSCREEN,
animations: ANIMATIONS,
},
bindings: Vec::new(),
}
}
#[scene]
#[entry]
pushdown fn Start() {}
fn GameView(game: Narrative) -> Element {
game_screen!(game = game)
}
fn Title() -> RootPage {
RootPage(column![
button!{on_click = || nav::start(Start, (), |_end| ()), t!["Start"]},
button!{on_click = || nav::dialog(ConfirmReset), t!["Reset"]},
])
}
// No way out.
fn ConfirmReset() -> Dialog {
Dialog(button!{on_click = || MUSIC.set(volume(60)), t!["Reset"]})
}

Fix: add a way out, e.g. button!{on_click = || nav::back(), t!["Cancel"]}, or bind a key to Action::Back (default_bindings() does).

W6004 unbounded-stack

Warning. Overlays open each other, or one opens itself, with nav::call. Each call pushes another mode, so the mode stack can grow without bound.

kix
use std::narrative::text::text_style;
use std::screen::size;
static TEXT_SPEED: Setting<Duration> = setting(ms(30));
static AUTO_DELAY: Setting<Duration> = setting(ms(1300));
static AUTO_DELAY_PER_CHAR: Setting<Duration> = setting(ms(60));
static SKIP_UNREAD: Setting<bool> = setting(false);
static MASTER: Setting<Volume> = setting(volume(100));
static MUSIC: Setting<Volume> = setting(volume(60));
static SOUND: Setting<Volume> = setting(volume(100));
static VOICE: Setting<Volume> = setting(volume(100));
static VOICE_SUSTAIN: Setting<bool> = setting(false);
static WAIT_FOR_VOICE: Setting<bool> = setting(true);
static WINDOW_OPACITY: Setting<Int<0, 100>> = setting(80);
static FULLSCREEN: Setting<bool> = setting(false);
static ANIMATIONS: Setting<bool> = setting(true);
const FONT: FontAsset = asset("fonts/default.otf");
fn main() -> System {
System {
title: "Example",
resolution: size(1280, 720),
start: Title,
game: GameView,
save_slots: 20,
auto_slots: 3,
backlog: 200,
font: FONT,
text: text_style(26, WHITE),
settings: Settings {
text_speed: TEXT_SPEED,
auto_delay: AUTO_DELAY,
auto_delay_per_char: AUTO_DELAY_PER_CHAR,
skip_unread: SKIP_UNREAD,
master: MASTER,
music: MUSIC,
sound: SOUND,
voice: VOICE,
voice_sustain: VOICE_SUSTAIN,
wait_for_voice: WAIT_FOR_VOICE,
window_opacity: WINDOW_OPACITY,
fullscreen: FULLSCREEN,
animations: ANIMATIONS,
},
bindings: Vec::new(),
}
}
#[scene]
#[entry]
pushdown fn Start() {}
fn GameView(game: Narrative) -> Element {
game_screen!(game = game)
}
fn Title() -> RootPage {
RootPage(column![
button!{on_click = || nav::start(Start, (), |_end| ()), t!["Start"]},
button!{on_click = || nav::call(Gallery), t!["Gallery"]},
])
}
fn Gallery() -> Overlay {
Overlay(column![
button!{on_click = || nav::call(Gallery), t!["Again"]},
button!{on_click = || nav::back(), t!["Back"]},
])
}

Fix: go back before opening the other overlay, or switch overlays without pushing, so the cycle doesn’t stack.

Last updated Oct 29, 2026