The RouteEngine is the core runtime for RVN visual novels. It manages state, processes actions, and coordinates rendering.
import createRouteEngine from "rvn-temp";
const engine = createRouteEngine({
handlePendingEffects: (effects) => {
// Process side effects (render, timers, etc.)
effects.forEach((effect) => {
switch (effect.name) {
case "render":
renderToScreen(engine.selectRenderState());
break;
case "handleLineActions":
engine.handleLineActions();
break;
// ... handle other effects
}
});
},
});Initializes the engine with project data and global settings.
engine.init({
initialState: {
projectData: {
resources: {
/* images, audio, etc */
},
story: {
initialSceneId: "scene1",
scenes: {
scene1: {
initialSectionId: "section1",
sections: {
section1: {
initialLineId: "line1", // optional, otherwise first line is used
lines: [
/* section lines */
],
},
},
},
},
},
},
},
});Localization is not implemented in the current runtime. The planned patch-based l10n model is documented in L10n.md.
The engine will:
- Create the system store with initial state
- Append a
rendereffect - Execute any actions on the initial line
- Trigger pending effects handler
- Clear pending effects
Returns a cloned snapshot of the full internal system state.
This is intended for tooling, debugging, capture harnesses, and devtools-style inspection. It should not be treated as the primary gameplay-facing API for normal runtime integration.
The returned value is a snapshot, not the live mutable store object.
const systemState = engine.selectSystemState();
console.log(systemState.global.nextLineConfig);
console.log(systemState.contexts.at(-1)?.pointers);Resolves authored layout references into renderer-facing fields without mutating the input value.
It resolves:
textStyleIdintotextStylecolorIdintofillimageIdintosrc- nested interaction references such as
hover.colorId,clickImageId, andrightClick.colorId
It uses the same strict rules as the engine render-state pipeline, so inline
authored textStyle, fill, and sprite src fields still throw.
import { resolveLayoutReferences } from "rvn-temp";
const resolvedElements = resolveLayoutReferences(layout.elements, {
resources: projectData.resources,
});Dispatches a single action to the system store.
// Advance to next line
engine.handleAction("nextLine");
// Jump to specific section
engine.handleAction("sectionTransition", { sectionId: "chapter_2" });
// Toggle auto mode
engine.handleAction("toggleAutoMode");Dispatches multiple actions from an object. Optionally accepts event context for _event.* payload bindings.
// Basic usage
engine.handleActions({
setNextLineConfig: {
manual: { enabled: false },
auto: { enabled: true, trigger: "fromComplete", delay: 2000 },
},
markLineCompleted: {},
});
// With event context (for slider/input events)
// Bindings like _event.value in action payloads get resolved
engine.handleActions(payload.actions, { _event: payload._event });When eventContext is provided, action payloads can use _event.* bindings to reference event values.
Action payloads can also reference ${variables.*} and they will be resolved at runtime.
eventContext only supports _event for event data; using event will throw.
Invalid _event.* bindings fail fast with an explicit error.
# In YAML layout definition
- id: volumeSlider
type: slider
min: 0
max: 100
change:
payload:
actions:
updateVariable:
id: setVolume
operations:
- variableId: volume
op: set
value: "_event.value" # Resolved to slider's current valueThe integration layer should pass event context when handling events:
eventHandler: (eventName, payload) => {
if (payload.actions) {
engine.handleActions(
payload.actions,
payload._event ? { _event: payload._event } : undefined,
);
}
};Layout authoring uses project resource references, not the renderer-facing fields that RouteGraphics consumes directly.
textStyleIdresolves throughresources.textStylesto renderertextStyleimageId/hoverImageId/clickImageIdresolve throughresources.imagesto renderersrc,hover.src, andclick.srccolorId/hover.colorId/click.colorId/rightClick.colorIdresolve throughresources.colorsto rendererfill
This resolution happens during render-state construction, before RouteGraphics parses the layout tree.
Layout text should be authored with textStyleId and resolved through resources.textStyles.
Authored inline textStyle objects in layout elements are rejected at render-state construction.
Text transparency should be authored on the text style resource with colorAlpha
and strokeAlpha, not baked into the shared color resource itself. Whole-node
transparency still uses the element alpha. resources.colors[*].hex must be
opaque hex only.
resources:
fonts:
fontDefault:
fileId: Arial
colors:
colorPrimary:
hex: "#FFFFFF"
textStyles:
body:
fontId: fontDefault
colorId: colorPrimary
colorAlpha: 0.9
fontSize: 24
fontWeight: "400"
fontStyle: normal
lineHeight: 1.2
strokeColorId: colorPrimary
strokeAlpha: 0.35
strokeWidth: 2
layouts:
dialogueLayout:
elements:
- id: dialogue-text
type: text
content: "${dialogue.content[0].text}"
textStyleId: bodyLayout sprite elements should be authored with imageId and optional
hoverImageId / clickImageId.
Authored inline sprite src and interaction hover.src / click.src fields are
rejected at render-state construction. Legacy url, hoverUrl, and clickUrl
fields are also rejected.
If resources.images[imageId] exists, the engine resolves the sprite to that
image resource's fileId. Otherwise, the rendered imageId string is passed
through directly, which allows dynamic values such as save preview image keys.
Before RouteGraphics parses the layout, the engine resolves these IDs to
sprite-facing src, hover.src, and click.src fields.
resources:
images:
buttonIdle:
fileId: button-idle.png
width: 400
height: 80
buttonHover:
fileId: button-hover.png
width: 400
height: 80
layouts:
titleLayout:
elements:
- id: start-button
type: sprite
imageId: buttonIdle
hoverImageId: buttonHoverLayout rect elements should be authored with colorId and optional
hover.colorId / click.colorId / rightClick.colorId.
Authored inline rect fill and interaction hover.fill / click.fill /
rightClick.fill fields are rejected at render-state construction.
Before RouteGraphics parses the layout, the engine resolves these IDs to
rect-facing fill fields.
resources:
colors:
panelBg:
hex: "#000000"
panelBgHover:
hex: "#141414"
layouts:
menuLayout:
elements:
- id: menu-panel
type: rect
width: 900
height: 420
opacity: 0.85
colorId: panelBg
hover:
colorId: panelBgHover
opacity: 0.9Processes actions attached to the current line. Called automatically on line changes.
// Line data structure
const line = {
id: "line_1",
actions: {
background: { resourceId: "bg_school" },
dialogue: { characterId: "protagonist", content: [{ text: "Hello!" }] },
bgm: { resourceId: "music_1" },
},
};Returns the current render state for the renderer.
const renderState = engine.selectRenderState();
// {
// elements: [{ id: 'story', type: 'container', children: [...] }],
// animations: [...],
// audio: [...]
// }Returns the current presentation state.
const presentationState = engine.selectPresentationState();
// {
// background: { resourceId: 'bg_school' },
// dialogue: { characterId: 'protagonist', content: [...] },
// bgm: { resourceId: 'music_1', loop: true }
// }| Action | Payload | Description |
|---|---|---|
nextLine |
- | Advance to the next line (respects nextLineConfig.manual) |
prevLine |
{ sectionId } |
Navigate to previous line (enters history mode) |
jumpToLine |
{ sectionId?, lineId } |
Jump to specific line |
sectionTransition |
{ sectionId } |
Transition to a different section |
| Action | Payload | Description |
|---|---|---|
startAutoMode |
- | Enable auto-advance mode |
stopAutoMode |
- | Disable auto-advance mode |
toggleAutoMode |
- | Toggle auto-advance mode |
startSkipMode |
- | Enable skip mode |
stopSkipMode |
- | Disable skip mode |
toggleSkipMode |
- | Toggle skip mode |
Playback timing semantics:
- Global
autoModewaits for the current line to complete before starting its_autoForwardTimedelay. - That completion is driven by Route Graphics
renderComplete, so revealing text and other tracked render work finish first. - Global
skipModedoes not use that completion gate; it advances on its own fast timer. nextLineConfig.autois separate and may usetrigger: "fromStart"ortrigger: "fromComplete"depending on authored behavior.
| Action | Payload | Description |
|---|---|---|
showDialogueUI |
- | Show the dialogue UI |
hideDialogueUI |
- | Hide the dialogue UI |
toggleDialogueUI |
- | Toggle dialogue UI visibility |
| Action | Payload | Description |
|---|---|---|
markLineCompleted |
- | Mark current line as completed |
setNextLineConfig |
{ manual?, auto? } |
Configure line advancement |
updateProjectData |
{ projectData } |
Replace project data |
| Action | Payload | Description |
|---|---|---|
addViewedLine |
{ sectionId, lineId } |
Mark line as viewed |
addViewedResource |
{ resourceId } |
Mark resource as viewed |
addToHistory |
{ item } |
Add entry to history sequence |
Seen-line semantics:
- The engine stores seen progress per section as a single frontier:
{ sectionId, lastLineId }. - The frontier line itself counts as seen.
- Any earlier line in the same section also counts as seen.
- The frontier is updated when a line is completed and when progression moves away from the current line.
| Action | Payload | Description |
|---|---|---|
saveSlot |
{ slotId, thumbnailImage? } |
Save game to a slot |
loadSlot |
{ slotId } |
Load game from a slot |
Save/load design, requirements, and storage boundaries are documented in SaveLoad.md.
Notes:
slotIdis the public action field; storage stringification is internal- save/load UIs can bind
slotIddirectly from layout templates such as${slot.slotId} - if slot identity comes from event data, use
_event.*bindings such asslotId: "_event.slotId" - example save/load UI copy should stay terse; prefer short labels like
Save,Load,Page 1,Saved,Empty, andImage thumbnailImageis integration-provided; the engine does not capture screenshots by itself- if a save action appears inside a multi-action event payload, the host should prepare/augment the
actionsobject and still callhandleActions(...)once for the whole batch
| Action | Payload | Description |
|---|---|---|
appendPendingEffect |
{ name, ...options } |
Queue a side effect |
clearPendingEffects |
- | Clear the effect queue |
The system store exposes these selectors (called internally):
| Selector | Parameters | Returns |
|---|---|---|
selectPendingEffects |
- | Array of pending effects |
selectCurrentPointer |
- | { currentPointerMode, pointer } |
selectCurrentLine |
- | Current line object |
selectSection |
{ sectionId } |
Section object |
selectAutoMode |
- | Boolean |
selectSkipMode |
- | Boolean |
selectDialogueUIHidden |
- | Boolean |
selectIsLineViewed |
{ sectionId, lineId } |
Boolean |
selectIsResourceViewed |
{ resourceId } |
Boolean |
selectNextLineConfig |
- | Config object |
selectSaveSlotMap |
- | Save slots object map |
selectSaveSlot |
{ slotId } |
Save slot data |
selectSaveSlotPage |
{ slotsPerPage? } |
Paged save slot list for UI |
Effects queued by actions for external handling:
| Effect | Description |
|---|---|
render |
Re-render the current state |
handleLineActions |
Process current line's actions |
Actions that can be attached to lines to control presentation:
| Action | Properties | Description |
|---|---|---|
background |
{ resourceId, animations? } |
Set background/CG |
dialogue |
{ characterId?, character?, content, mode?, ui?, clear? } |
Display dialogue |
character |
{ items } |
Display character sprites. Each item can have optional x and y to override transform position |
visual |
{ items } |
Display visual elements |
bgm |
{ resourceId, loop?, volume?, delay? } |
Play background music |
sfx |
{ items } |
Play sound effects |
voice |
{ fileId, volume?, loop? } |
Play voice audio |
animation |
{ ... } |
Apply animations |
layout |
{ resourceId } |
Display layout |
control |
{ resourceId } |
Activate control bindings and control UI |
choice |
{ resourceId, items } |
Display choice menu |
cleanAll |
true |
Clear all presentation state |