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
}
});
},
});For deterministic tests, createRouteEngine also accepts a randomSource
with a synchronous nextUint32() method. It must return an integer from 0
through 4294967295. Production hosts can omit it; the engine uses
crypto.getRandomValues when available and falls back to Math.random for
compatibility.
Initializes the engine with project data and global settings.
engine.init({
namespace: "my-visual-novel",
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 */
],
},
},
},
},
},
},
},
});For browser-backed save/load hydration, the runtime also exports
createIndexedDbPersistence({ namespace }). Use the same namespace both when
loading persisted data before init and when calling engine.init(...) so
different visual novels on the same domain do not share persistence. The
returned adapter also exposes clear() to delete persisted data for that
namespace.
Persistence assumes one active engine writer per namespace. Saves replace the stored slot map with that engine's map; use distinct namespace keys for independent engines.
Initialization may also receive complete imported L10n packages through
initialState.l10nData. The canonical project is the default; an existing
device preference under initialState.global.runtime.localizationPackageId
restores an imported package. Authored layouts receive package options and can
dispatch updateLocalizationPackage to switch and render. The patch format,
package layout, and runtime behavior are documented in L10n.md.
Projects can override the initial values of device-level preferences under
projectData.config.runtimeDefaults:
config:
runtimeDefaults:
dialogueTextSpeed: 50
autoForwardDelay: 1000
autoForwardSpeed: 50
skipUnseenText: false
skipTransitionsAndAnimations: false
soundVolume: 50
musicVolume: 50
muteAll: falseEvery field is optional. Omitted fields retain the engine default. During initialization, a persisted device preference overrides the project default for that field. Project defaults are only initialization fallbacks: changing project data, resetting story progress, loading a slot, or rolling back does not overwrite the active device preferences.
autoForwardSpeed, soundVolume, and musicVolume accept values from 0 to
100; autoForwardDelay is a non-negative number of milliseconds. Runtime
mode flags, context UI state, and localizationPackageId are intentionally not
project-configurable. The canonical project remains the initial localization
unless the device has a saved localization selection.
During initialization, the engine:
- Create the system store with initial state
- Append a
rendereffect - Execute any actions on the initial line
- Trigger pending effects handler
- Clear pending effects
Calling init(...) again replaces the active runtime generation. Engine-owned
timers, render-completion ownership, and pending asynchronous renderer input
from the previous generation are invalidated before the replacement begins.
If validation of the replacement state fails, the existing generation remains
active.
Section IDs must be unique across scenes, and line IDs must be unique within
each section. init(...) and updateProjectData reject duplicates with the
scene, section, and conflicting line indexes before replacing active state.
Different sections may reuse a line ID.
An initialization or action dispatch may process at most 1,000 synchronous
effect batches before yielding back to the host. Exceeding that limit throws
an error naming the current section and line and invalidates playback timers.
The error has code: "routing_cycle" and pointer: { sectionId, lineId }
for that line; identify it by code rather than by message text.
This bounds immediate routing cycles, including conditional cycles. Each
completed dispatch gets a fresh budget, so loops that pause for player input
continue to work. After a limit error, the host can reinitialize, dispose, or
reset to a safe section with resetStoryAtSection.
Stops the active engine generation and releases engine-owned timers and event
ownership. Disposal is idempotent. Renderer events and story actions from the
disposed generation are rejected. Read-only state remains available through
selectSystemState(). A later successful init(...) starts a fresh generation
on the same engine instance.
Hosts should call dispose() before permanently discarding an engine. Pending
persistence writes that were already handed to the persistence adapter are not
cancelled.
Computed variables are derived read-only values declared under
resources.variables[*].computed. They are exposed through the same
variables.* template namespace as stored variables, but are not persisted or
updated directly.
The locked authored interface is documented in ComputedVariables.md.
Achievements are stable, platform-agnostic definitions under
resources.achievements. Story and UI actions reference only the Route Engine
resource ID. Public selectors expose the authored definitions, and ordered
effects let external consumers integrate them without putting platform details
in Route Engine.
The resource, selector, action, and effect primitives are documented in Achievements.md.
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. The one
exception is the narrow inline textStyle allowed on a run inside the content
array of a text or text-revealing element; see
Inline Text Run Styles.
import { resolveLayoutReferences } from "rvn-temp";
const resolvedElements = resolveLayoutReferences(layout.elements, {
resources: projectData.resources,
});Evaluates computed variables with the same logic used by the engine.
import { resolveComputedVariables } from "rvn-temp";
const variables = resolveComputedVariables({
projectData,
variables: {
hp: 40,
maxHp: 100,
},
runtime,
});Pass either full projectData or variableConfigs. The returned object merges
stored variables with resolved computed variables and ignores stale computed
keys from the input variables object.
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 },
},
startAutoMode: {},
});
// 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.
Every RouteGraphics visual element in a layout may own an ordered filters
array. Shader effects stay inline with the element that uses them. Route Engine
preserves the filter configuration through template and resource resolution;
RouteGraphics validates and compiles the WebGL or WebGPU source.
Set time: true when the shader needs RouteGraphics' deterministic uTime
clock. This does not require a Route Engine animation resource.
resources:
layouts:
shadedHud:
elements:
- id: portrait
type: sprite
imageId: portrait
filters:
- id: shade
type: shader
time: true
parameters:
strength: 0.6
source:
webgl:
fragment: |
# GLSL fragment source
webgpu:
source: |
# WGSL source with mainVertex and mainFragmentFilter IDs must be unique within their owning element. Shader parameters,
passes, textures, pipeline settings, and source ABI follow the RouteGraphics
inline shader interface. To filter a complete layout, place its elements under
a container and attach the filter to that container. Route Engine treats filter
contents as opaque renderer data, so parameter and texture names may overlap
Engine fields such as textStyle or textStyleId. Filter arrays may use the
same $if and $for template entries as other layout arrays.
Background actions, character items, and visual items may also own an ordered
filters array. Route Engine forwards it to the generated root render target,
so the shader applies to the complete background resource, composed character,
or visual item instead of an individual child.
background:
resourceId: forest
filters:
- id: color-grade
type: shader
parameters:
saturation: 0.5
source:
webgl:
fragment: |
# GLSL fragment source
webgpu:
source: |
# WGSL source with mainVertex and mainFragment
character:
items:
- id: alice
transformId: center
sprites:
- id: body
resourceId: aliceBody
filters:
- id: character-glow
type: shader
time: true
source:
webgl:
fragment: |
# GLSL fragment source
webgpu:
source: |
# WGSL source with mainVertex and mainFragment
visual:
items:
- id: fog
resourceId: fogTexture
transformId: fullscreen
filters:
- id: fog-distortion
type: shader
time: true
source:
webgl:
fragment: |
# GLSL fragment source
webgpu:
source: |
# WGSL source with mainVertex and mainFragmentAction filters are instance appearance. A filters-only continuation action
updates the existing target without repeating its resource, transform, or
character sprite parts. Omission on a continuation patch preserves the current
filters; filters: [] clears them. Explicitly reauthoring a background
resourceId, a non-empty character sprites array, or a complete visual
subject creates a fresh instance and resets omitted filters along with all other
omitted instance state. For a background with a renderable resource and a
backing colorId, filters apply to the resource target. A color-only background
applies filters to its color rect.
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.
The only exception is a content run, described in
Inline Text Run Styles.
Text transparency should be authored on the text style resource with colorAlpha,
strokeAlpha, and shadow.alpha, 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
shadow:
colorId: colorPrimary
alpha: 0.75
blur: 6
offsetX: 2
offsetY: 3
layouts:
dialogueLayout:
elements:
- id: dialogue-text
type: text
content: "${dialogue.content[0].text}"
textStyleId: bodyA layout element may not author an inline textStyle. A run inside the
content array of a text or text-revealing element is different: it is the
writer's text rather than layout design, so it may carry a narrow emphasis
override. This is how dialogue content such as bold, italic, underlined, or
accented text reaches the renderer:
dialogue:
content:
- text: "Plain "
- text: "bold "
textStyle:
fontWeight: bold
- text: "accent"
textStyle:
fill: "#b45309"Rules:
- Only these
textStylefields are accepted on a run. Any other field, such asfontSize,fontFamily, orshadow, throws with the path of the run. UsetextStyleIdfor those.fontWeight:normal,bold,bolder,lighter, or a number from 1 to 1000. A numeric string such as"700"is also accepted.fontStyle:normal,italic, oroblique.textDecoration:noneorunderline. Underline needs a Route Graphics release that drawstextDecoration(1.48.0 or later); earlier releases ignore it.fill: an opaque hex color such as#b45309or#b50. Color names,rgba(), and hex colors with alpha are rejected. Transparent text belongs on a text style resource withcolorAlpha.
- The engine passes the run's
textStyleto Route Graphics with only the fields that were authored (a short hexfillis expanded to six digits). Route Graphics merges it over the element's own text style, so a run with onlyfontWeight: boldkeeps the element's font, size, and color. - A run with both
textStyleIdandtextStyleresolvestextStyleIdthroughresources.textStylesfirst, then replaces the matching fields of the resolved style with the inline fields. The run then carries the complete resolved style. An inlinefillreplaces the text style's fill together with itscolorAlpha. - The exception covers only the run itself.
textStyleon the element, on itshover,click, orrightClick, onfurigana, or on an element of any other type still throws.furiganakeeps usingtextStyleId. - Once templates are rendered, the engine cannot tell dialogue content from a
run written directly in a layout's
contentarray, so both follow the same rule. The same applies totext.contentruns of legacy text visuals.
Layout 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
alpha: 0.85
colorId: panelBg
hover:
colorId: panelBgHover
opacity: 0.9Authored layout element ids are not rendered verbatim. Each rendered layout
instance namespaces its element ids under its owning semantic slot, using the
slot's own stable root id: @layout/layout-<resourceId>--<authoredId> for the layout
action, @layout/overlayStack-<index>--<authoredId> for authored overlay stack
entries, @layout/bg-cg-background-container--<authoredId> for layout backgrounds,
@layout/visual-<itemId>--<authoredId> for visual items, @layout/dialogue-container--<authoredId>
for dialogue UIs, @layout/choice-container--<authoredId> for choices,
@layout/form-container--<authoredId> for forms, @layout/control-<resourceId>--<authoredId>
for control layouts, and @layout/confirmDialog--<authoredId> for confirm dialogs.
The reserved @layout/ prefix separates authored descendants from engine root
containers, even when a visual or resource id contains --. The contract is
intentionally unconditional:
- an instance's rendered ids depend only on its own slot identity — never on traversal order or on how many other instances currently share the resource — so adding, removing, or re-rendering unrelated layouts, overlays, or conditionals never rekeys another instance's element ids
- the same layout resource can safely back several slots at once (for example
a
layoutaction plus a pushed overlay); every occurrence stays unique and addressable - native DOM widgets keyed by these ids (such as text inputs) keep their DOM identity, typed value, and focus across unrelated presentation changes
- slot keys track the supported overlay stack operations: pushing, popping, or replacing the top entry keeps existing entries' indexes, so their namespaced ids are stable
- authored ids repeated inside one instance receive deterministic
-1,-2, ... suffixes in authored order - a literal
--inside an authored id or resource id is percent-escaped (%2D%2D), keeping--unambiguous as the namespace separator; ordinary hyphenated ids are unchanged
Root container ids themselves (layout-<resourceId>, overlayStack-<index>,
form-container, ...) are engine-owned and never renamed, and authored
resource definitions and variable-bound element objects are never mutated.
Background actions can set a persistent solid backing color with colorId.
The color resolves through resources.colors and renders behind the background
image, video, spritesheet, or layout. If no background colorId has been set,
the backing color uses screen.backgroundColor, which defaults to black.
screen:
width: 1920
height: 1080
backgroundColor: "#000000"
resources:
colors:
nightBackdrop:
hex: "#05070D"
story:
scenes:
scene1:
sections:
intro:
lines:
- id: line1
actions:
background:
colorId: nightBackdrop
resourceId: forestA background resourceId can reference resources.spritesheets. Use
animationName to select a named animation; when it is omitted, the first
defined animation is used. animationSpeed and loop override the defaults on
that animation.
background:
resourceId: animatedSky
animationName: storm
animationSpeed: 0.4
loop: trueProcesses 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",
character: {
name: "Hero",
},
persistCharacter: true,
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: [{ id: 'channel:bgm', type: 'audio-channel', children: [...] }]
// }Returns the current presentation state.
const presentationState = engine.selectPresentationState();
// {
// background: { resourceId: 'bg_school' },
// dialogue: {
// characterId: 'protagonist',
// character: { name: 'Hero' },
// persistCharacter: true,
// content: [...]
// },
// bgm: { resourceId: 'music_1', loop: true }
// }Returns the per-line presentation diff for an entire section.
By default each line entry only includes changes. Pass
includePresentationState: true to also include the full end-state
presentationState after that line has been applied.
Background changes are split into independent persistent layers. resource
tracks resourceId and its related fields, while color tracks colorId.
Background animation selections are playback instructions and are not included
in this diff.
Dialogue UI changes are reported as dialogue. Speaker sprite changes are
reported separately as dialogueSprite, with add, update, or delete as
the change type.
const sectionLineChanges = engine.selectSectionLineChanges({
sectionId: "section1",
includePresentationState: true,
});
// {
// lines: [
// {
// id: "line-1",
// changes: {
// background: {
// resource: { changeType: "add", data: { resourceId: "bg_school" } },
// color: { changeType: "add", data: { colorId: "night" } },
// },
// },
// presentationState: { ... }
// }
// ]
// }handleActions and every nested action batch use the following canonical
schedule. YAML/JavaScript object property order has no effect. Phases and the
actions inside each phase run from top to bottom.
- Cleanup
cleanAll
- State
updateVariableupdateFormFieldupdateLocalizationPackage- integer
random
- Decision
submitFormcancelForm- weighted
random conditional
- Presentation
screenbackgrounddialoguecharactervisualchoiceformsfxbgmvoicecontrollayout
- Runtime
setNextLineConfigstartAutoModestopAutoModetoggleAutoModestartSkipModestopSkipModetoggleSkipModeshowDialogueUIhideDialogueUItoggleDialogueUIsetDialogueTextSpeedsetAutoForwardDelaysetAutoForwardSpeedsetSkipUnseenTextsetSkipTransitionsAndAnimationssetSoundVolumesetMusicVolumesetMuteAllsetSaveLoadPaginationincrementSaveLoadPaginationdecrementSaveLoadPaginationsetMenuPagesetMenuEntryPointshowConfirmDialoghideConfirmDialogpushOverlaypopOverlayreplaceLastOverlayclearOverlayscompleteAchievementsetAchievementProgressshowImageGalleryVariantmoveToPreviousImageGalleryVariantmoveToNextImageGalleryVariantclearImageGallerySelectionmoveToImageGalleryPagemoveToNextImageGalleryPagemoveToPreviousImageGalleryPageplayMusicRoomTrackplayMusicRoompauseMusicRoomstopMusicRoomseekMusicRoomplayPreviousMusicRoomTrackplayNextMusicRoomTrackclearMusicRoomSelectionmoveToMusicRoomPagemoveToNextMusicRoomPagemoveToPreviousMusicRoomPagefinishSceneReplaymoveToSceneReplayPagemoveToNextSceneReplayPagemoveToPreviousSceneReplayPage
- Persistence
saveSlot
- Navigation
loadSlotrollbackByOffsetrollbackToLineresetStoryAtSectionsectionTransitionjumpToLinestartSceneReplayexitSceneReplaynextLine
Engine-only and test store actions have fixed positions in the same schedule:
- Cleanup, before
cleanAll:clearPendingEffects. - State, before
updateVariable:addViewedLine,addViewedResource. - State, after
updateFormField:updateProjectData. - State, after
updateLocalizationPackage:markLineCompleted. - Runtime, after
moveToPreviousSceneReplayPage:musicRoomSoundReady,musicRoomSoundProgress,musicRoomSoundComplete,musicRoomSoundError,appendPendingEffect,beginRollbackActionBatch,endRollbackActionBatch,ensureRandomReplayOccurrence,recordRandomOutcome,markRollbackCheckpointTransient,markSavedRollbackCheckpointTransient,recordCurrentDialogueHistory. - Navigation, after
nextLine:nextLineFromSystem.
Templates are resolved immediately before their scheduled action, so later
phases observe state produced by earlier phases. An integer random therefore
always writes its declared variable before conditional, even if
conditional appears first in YAML. Weighted random is a decision because it
executes a selected nested action batch.
Nested batches use the same schedule. A navigation selected inside a nested batch is deferred until the outermost batch reaches its Navigation phase. This lets outer Runtime and Persistence actions settle before navigation executes; the navigation is then terminal.
A batch containing multiple direct navigation actions is rejected before any action runs. If separately nested actions select more than one reachable route, the whole transaction is rejected before any route executes. Conditional and weighted branches remain the supported way to author mutually exclusive navigation.
Unknown host-only batch actions are ordered by code-unit name after Runtime and
before Persistence. Authored project schemas reject unknown actions. Direct
handleAction dispatch remains immediate and does not use the batch schedule.
| Action | Payload | Description |
|---|---|---|
nextLine |
- | Advance to the next line (respects nextLineConfig.manual) |
rollbackByOffset |
{ offset? } |
Roll back by eligible landing points (-1 means Back) |
rollbackToLine |
{ sectionId, lineId } |
Roll back to a specific line in the rollback timeline |
jumpToLine |
{ sectionId?, lineId } |
Jump to specific line |
sectionTransition |
{ sectionId, screen? } |
Transition to a different section |
sectionTransition.screen uses the same shape as line-level screen:
sectionTransition:
sectionId: chapter2
screen:
animations:
resourceId: screenCrossFadeThis transition is scoped to the edge between sections. If both
sectionTransition.screen and the destination line's screen.animations are
defined, the sectionTransition.screen animation wins for the first destination
render only.
Rollback design treats a source line that invokes sectionTransition during
line-entry processing as transient. Player-facing Back must skip that source
and restore the preceding settled line in one action. A transition triggered
later from an already-presented interaction does not disqualify that source
line. This target-selection behavior is specified in Rollback.md
and is applied by rollbackByOffset and its availability selectors.
Use conditional to evaluate ordered branches and execute the first matching
branch. when uses Jempl semantic JSON condition syntax. String expression
conditions are not supported. A branch without when is treated as else and
should be last.
actions:
conditional:
branches:
- when:
gte:
- var: variables.trust
- 70
actions:
jumpToLine:
lineId: trustedRoute
- actions:
jumpToLine:
lineId: guardedRouteThe same action can be used inside choice click payloads:
events:
click:
payload:
actions:
conditional:
branches:
- when:
eq:
- var: variables.role
- admin
actions:
jumpToLine:
lineId: adminRouteThe conditional runs the first matching branch, or its default branch when one
is authored. If nothing matches and there is no default, it runs no branch
actions. In every case, the engine automatically continues once after the
entire action batch finishes. Invoking sectionTransition or
resetStoryAtSection suppresses that continuation. Any other action that
changes the current pointer, such as jumpToLine or an advancing nextLine,
also suppresses it so the batch cannot skip a second line. Nested conditionals
coalesce into the same single batch continuation. This is an immediate
control-flow advance: hidden dialogue does not consume it and remains hidden.
When skip mode cannot pass unseen content, the engine enters that destination
and stops skip there. Active choice and form authorization still applies.
Condition comparisons are strict and never coerce operands. eq and neq
compare both type and value, so 1 and "1" are different. Objects and arrays
compare by identity rather than deep contents. gt, gte, lt, and lte
compare only two finite numbers or two strings; mixed types and all other
operand combinations evaluate to false. A missing path is distinct from an
explicit null value; there is currently no dedicated exists operator. These
rules apply to conditional-action semantic JSON, not Jempl string expressions
inside layout templates.
A line-authored conditional that automatically continues during line entry is a transient rollback source. Player-facing Back must skip it rather than rendering or pausing on an empty conditional line. Conditionals triggered later from an already-presented interaction leave that source eligible for Back. See Rollback.md; player-facing rollback skips the transient source in the same action.
Use random to generate a uniformly distributed integer directly into a
declared writable context number variable:
actions:
random:
distribution:
type: integer
min: 1
max: 100
variableId: randomNumber
conditional:
branches:
- when:
gte:
- var: variables.randomNumber
- 75
actions:
jumpToLine:
lineId: lockOpened
- actions:
jumpToLine:
lineId: lockFailedBoth bounds are inclusive. Every integer from min through max has equal
probability. Integer random is in the State phase and conditional is in the
Decision phase, so the generated number is stored first regardless of their
YAML property order.
Distribution configuration is fixed authored data: numeric fields accept only literal numbers and do not resolve variables or action templates.
Weighted selection is branch-only: each outcome contains its own action batch, with no authored value or result variable:
actions:
random:
distribution:
type: weighted
outcomes:
- weight: 70
actions:
jumpToLine:
lineId: commonReward
- weight: 30
actions:
jumpToLine:
lineId: rareRewardLike conditional, random automatically continues once unless the outer
batch or selected weighted actions navigate. Line-authored outcomes are
recorded with rollback history so save/load and rollback replay the stored
integer or weighted branch without rerolling. Chance routing uses two weighted
outcomes. The full contract and authoring-tool interface are in
RandomAction.md.
| 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 a length-aware delay. The delay combinesruntime.autoForwardDelaywith grapheme-based reading time adjusted byruntime.autoForwardSpeed. The result is capped at 20 seconds unless the configured base delay itself is higher; in that case, the higher base delay is preserved as the effective delay. - 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.
The built-in effects handler reconciles auto, skip, and authored auto timing as one authoritative schedule. It owns at most one physical ticker callback per engine even when multiple logical deadlines are active. If two deadlines become due on the same tick, the engine performs one progression attempt; it does not skip a second line.
Logical deadlines retain their exact remaining duration while their descriptor
is unchanged. This includes an authored fromStart deadline when a manual
click only completes the current reveal. Auto and authored deadlines restart
for a new line occurrence or changed delay; authored timing also restarts when
its trigger changes, while auto restarts for an accepted mode restart or
changed resolved text. Skip timing belongs to the active skip session and
therefore continues across ordinary line changes until it becomes due or skip
mode restarts. Duration accumulation uses exact binary64 arithmetic, so
repeated fractional deltas and very large finite delays do not drift at the due
boundary.
Timer ownership is private runtime metadata. It is absent from
selectSystemState(), save slots, persistence payloads, and render state.
Loading, reinitializing, or entering a fresh occurrence derives a new schedule
from the settled semantic state; partial elapsed time is never serialized.
Rollback deliberately suppresses a restored line's authored auto deadline
until that line occurrence or its authored auto configuration changes.
The engine reconciles only after an outer action batch and all of its effects
settle. Effect or reconciliation failures send an unsettled schedule and
remove active timing fail-closed. A failed progression that did not commit is
not retried repeatedly with the same ownership descriptor. A committed
destination may schedule again after a later successful reconciliation.
Custom effect handlers remain compatible in two modes:
- A handler without
reconcilePlaybackScheduleV1receives the existing six timer start/clear effects unchanged. - A handler that defines
reconcilePlaybackScheduleV1must also define synchronousresetanddisposemethods. The engine then withholds the six legacy timer effects and calls the reconciler with a complete cloned schedule. Capabilities are captured when the engine is created, and the reconciler must complete synchronously without mutating, initializing, or disposing the engine reentrantly.
The V1 reconciler receives one of these shapes:
{
contractVersion: 1,
status: "settled",
lineEntryId: 42,
timers: {
auto: {
owner: { sessionId: 3, lineEntryId: 42 },
delayMs: 1800,
contentKey: "Resolved dialogue text",
},
skip: null,
authored: {
owner: { lineEntryId: 42 },
delayMs: 1000,
trigger: "fromStart",
},
},
}
// Fail-closed invalidation
{
contractVersion: 1,
status: "unsettled",
lineEntryId: 42,
timers: null,
}All fields and ownership IDs are authoritative and opaque to the host. A reconciler should preserve elapsed time only when a logical descriptor is field-for-field equivalent to the currently installed descriptor.
| Action | Payload | Description |
|---|---|---|
setDialogueTextSpeed |
{ value } |
Set dialogue reveal speed |
setAutoForwardDelay |
{ value } |
Set non-negative base auto delay in milliseconds |
setAutoForwardSpeed |
{ value } |
Set length-aware auto speed from 0 to 100 |
setSkipUnseenText |
{ value } |
Set whether skip mode may pass unseen dialogue |
setSkipTransitionsAndAnimations |
{ value } |
Set whether authored transitions and animations are skipped |
setSoundVolume |
{ value } |
Set Voice/SFX/visual-video volume from 0 to 100 |
setMusicVolume |
{ value } |
Set BGM/music-room/background-video volume from 0 to 100 |
setMuteAll |
{ value } |
Set global audio mute |
updateLocalizationPackage |
{ l10nId } |
Select an imported package, or null for canonical content |
setSaveLoadPagination |
{ value } |
Set the active context's one-based save/load page |
incrementSaveLoadPagination |
{} |
Increment the active save/load page |
decrementSaveLoadPagination |
{} |
Decrement the active save/load page, clamped to page 1 |
setMenuPage |
{ value } |
Set the active context's menu page string |
setMenuEntryPoint |
{ value } |
Set the active context's menu entry-point string |
The first nine actions update device-persisted runtime preferences. Save/load and menu navigation values are context-local and included in save slots. Rollback reconstructs these context runtime values by replaying their authored and interaction-originated setters up to the selected checkpoint. L10n selection behavior is documented in L10n.md.
| Action | Payload | Description |
|---|---|---|
showDialogueUI |
{} |
Show the dialogue UI |
hideDialogueUI |
{} |
Hide the dialogue UI |
toggleDialogueUI |
{} |
Toggle dialogue UI visibility |
pushOverlay |
{ resourceId } |
Push a layout onto the authored overlay stack |
popOverlay |
{} |
Remove the last overlay |
replaceLastOverlay |
{ resourceId } |
Replace the last overlay |
clearOverlays |
{} |
Clear overlays, confirm dialog, and form drafts |
showConfirmDialog |
{ resourceId, confirmActions, cancelActions? } |
Show a transient layout-backed confirm dialog |
hideConfirmDialog |
{} |
Hide the current confirm dialog |
Confirm-dialog normalization, deferred actions, lifecycle, and rendering are documented in ConfirmDialog.md.
| Action | Payload | Description |
|---|---|---|
updateFormField |
{ formKey, field, value? } |
Update a transient draft field |
submitForm |
{ formKey, actions? } |
Validate and commit the active form, then act |
cancelForm |
{ formKey, actions? } |
Clear the active form draft and run follow-ups |
| Action | Payload | Description |
|---|---|---|
setNextLineConfig |
{ manual?, auto? } |
Configure line advancement |
updateProjectData |
{ projectData } |
Replace project data |
resetStoryAtSection |
{ sectionId, screen? } |
Reset story-local state and enter a section |
| Action | Payload | Description |
|---|---|---|
addViewedLine |
{ sectionId, lineId } |
Mark line as viewed in account state |
addViewedResource |
{ resourceId } |
Mark resource as viewed in account state |
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.
- Account-level viewed state is persisted outside save slots as
global.accountViewedRegistry. - Skip-unseen checks use account-level viewed state;
runtime.skipUnseenTextonly controls whether skip may pass unseen account content.
| Action | Payload | Description |
|---|---|---|
showImageGalleryVariant |
{ groupId, variantId? } |
Select an unlocked variant and its containing page |
moveToPreviousImageGalleryVariant |
{} |
Select the previous unlocked variant |
moveToNextImageGalleryVariant |
{} |
Select the next unlocked variant |
clearImageGallerySelection |
{} |
Clear selection while retaining the current page |
moveToImageGalleryPage |
{ pageIndex } |
Move to a zero-based page and clear selection |
moveToNextImageGalleryPage |
{} |
Move to the next page without wrapping |
moveToPreviousImageGalleryPage |
{} |
Move to the previous page without wrapping |
Gallery actions use the optional singleton resources.imageGallery. Locked
state comes from the existing viewed-resource registry. Well-formed actions are
no-ops when the gallery or requested target is unavailable; malformed payloads
throw. If an action target exactly matches a declared group or variant ID, the
ID is treated literally even when it resembles a template; targets without an
exact declared match continue through normal action-template resolution.
| Action | Payload | Description |
|---|---|---|
playMusicRoomTrack |
{ trackId } |
Select and play/restart an unlocked track |
playMusicRoom |
{} |
Resume or start the selected track |
pauseMusicRoom |
{} |
Pause and preserve the renderer-owned cursor |
stopMusicRoom |
{} |
Stop at zero while retaining selection |
seekMusicRoom |
{ positionMs } |
Seek to segment-relative milliseconds |
playPreviousMusicRoomTrack |
{} |
Play the previous unlocked track without wrapping |
playNextMusicRoomTrack |
{} |
Play the next unlocked track without wrapping |
clearMusicRoomSelection |
{} |
Clear selection and restore story BGM |
moveToMusicRoomPage |
{ pageIndex } |
Browse a zero-based page |
moveToNextMusicRoomPage |
{} |
Browse the next page without wrapping |
moveToPreviousMusicRoomPage |
{} |
Browse the previous page without wrapping |
The optional singleton resources.musicRoom has one transient player and no
layout association. Layouts consume the computed musicRoom template root and
dispatch these actions. Locks reuse addViewedResource with each track's
soundId. Full catalog, projection, playback, validation, and audio-focus
semantics are documented in MusicBox.md.
| Action | Payload | Description |
|---|---|---|
startSceneReplay |
{ sceneId } |
Start an unlocked scene in a fresh context |
finishSceneReplay |
{} |
Unlock normally, or finish an active replay |
exitSceneReplay |
{} |
Exit immediately and restore the caller |
moveToSceneReplayPage |
{ pageIndex } |
Browse a zero-based replay page |
moveToNextSceneReplayPage |
{} |
Browse the next page without wrapping |
moveToPreviousSceneReplayPage |
{} |
Browse the previous page without wrapping |
The optional singleton resources.sceneReplay has no layout association.
Layouts consume the computed sceneReplay template root, including each
entry's locked flag. Normal-story finish markers persist scene unlocks at
account scope. Replays use a fresh context, suppress persistent progress, and
restore the caller when they end.
See ReplayScene.md for the full contract.
| Action | Payload | Description |
|---|---|---|
completeAchievement |
{ resourceId } |
Declare a boolean or number achievement complete |
setAchievementProgress |
{ resourceId, current } |
Report absolute progress for a number achievement |
Achievement actions enqueue external effects and do not store player achievement state. See Achievements.md for resource, validation, progress, and host-integration semantics.
| 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. Destructive fresh-start navigation semantics are documented in ResetStoryAtSection.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
These actions exist inside the store/runtime but are not part of the stable authored/public API surface:
| Action | Payload | Description |
|---|---|---|
markLineCompleted |
- | Internal render-complete transition |
nextLineFromSystem |
internal options | Internal engine-driven advance |
appendPendingEffect |
{ name, ...options } |
Queue a side effect |
clearPendingEffects |
- | Clear the effect queue |
Use these only if you are extending engine internals or writing engine-level tests.
The system store exposes these selectors (called internally):
| Selector | Parameters | Returns |
|---|---|---|
selectPendingEffects |
- | Array of pending effects |
selectCurrentPointer |
- | { currentPointerMode: "read", pointer } |
selectCurrentLine |
- | Current line object |
selectSection |
{ sectionId } |
Section object |
selectAutoMode |
- | Boolean |
selectSkipMode |
- | Boolean |
selectDialogueUIHidden |
- | Boolean |
selectIsLineAccountViewed |
{ sectionId, lineId } |
Account-level viewed boolean |
selectIsResourceAccountViewed |
{ resourceId } |
Account-level viewed boolean |
selectNextLineConfig |
- | Config object |
selectLineIdByOffset |
{ offset? } |
Relative line; negative offsets skip transient entries |
selectCanRollback |
- | Whether an earlier landing point exists |
selectAchievements |
- | Cloned achievement resource map |
selectAchievement |
{ resourceId } |
Cloned achievement resource or undefined |
selectImageGallery |
- | Computed singleton image-gallery projection or null |
selectMusicRoom |
- | Computed singleton music-room projection or null |
selectSceneReplay |
- | Computed singleton scene-replay projection or null |
selectIsSceneReplayActive |
- | Whether the current context is a replay |
selectRuntime |
- | Cloned authored runtime.* projection |
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 |
applyScopedDataUpdates |
Persist ordered scoped data operations |
completeAchievement |
Notify the host of achievement completion |
setAchievementProgress |
Notify the host of absolute achievement progress |
applyScopedDataUpdates is a public runtime-facing persistence contract. Its full interface and semantics are documented in ScopedDataUpdates.md.
Built-in effect handling notes:
createEffectsHandler(...)coalesces only the latest occurrence of replaceable built-in effects such asrender,handleLineActions, full-snapshot persistence effects, and legacy timer start/clear effects before translating them.- With the built-in V1 playback capability, the engine filters legacy timer effects and supplies a complete authoritative schedule instead. Custom handlers that do not opt into V1 continue to receive the legacy effects.
applyScopedDataUpdatesis incremental and ordered, so it must not be last-write coalesced by effect name.- Achievement effects are external, ordered, and never coalesced by effect name.
- Unknown effect names are not silently dropped;
createEffectsHandler(...)throws unless you providehandleUnhandledEffect. - The coalescing rule is specific to the built-in effect handler, not the store queue itself.
Actions that can be attached to lines to control presentation:
| Action | Properties | Description |
|---|---|---|
screen |
{ alpha?, opacity?, blur?, animations? } |
Set whole-screen appearance or transition. alpha/blur apply to the composed story frame |
background |
{ resourceId?, colorId?, transformId?, x?, y?, anchorX?, anchorY?, scaleX?, scaleY?, flipX?, flipY?, rotation?, originX?, originY?, alpha?, opacity?, blur?, filters?, animations? } |
Set background/CG. Transform fields are renderer pixels/unitless multipliers/degrees; blur: null clears background blur |
dialogue |
{ characterId?, character?, character.sprite?, persistCharacter?, persistSprite?, content, append?, mode?, ui?, clear? } |
Display dialogue |
character |
{ items } |
Display character sprites. Each item can set transform overrides, alpha, blur, and filters |
visual |
{ items } |
Display resource-backed or inline-layout visual elements. Each item can set layer, transform, alpha, blur, filters, and animations |
bgm |
{ sounds, loop?, volume?, muted?, pan? } |
Control the persistent, multi-sound BGM channel |
sfx |
{ channels: [{ id, sounds, applyMode?, loop?, volume?, muted?, pan? }] } |
Play any number of single-line or persistent SFX channels, each with its own sounds |
voice |
{ sounds, loop?, volume?, muted?, pan? } |
Control the line-scoped, multi-sound Voice channel; resources resolve from the current scene |
animation |
{ ... } |
Apply animations |
layout |
{ resourceId } |
Display layout |
control |
{ resourceId } |
Activate control bindings and control UI |
choice |
{ resourceId, items } |
Display choice menu |
form |
{ resourceId, fields, submitActions?, cancelActions? } |
Display a blocking multi-input form |
cleanAll |
true |
Clear all presentation state |
Animation selections use animations.resourceId plus optional
animations.playback. playback.speed is a unitless multiplier: 1 is normal,
2 is twice as fast, and 0.5 is half speed. playback.continuity defaults to
render-scoped behavior when omitted.
BGM update effects use numeric keyframes for volume, pan, and
playbackRate. The final keyframe must be absolute. When the line keeps or
starts music, its value becomes the persistent BGM value after the effect
finishes; intermediate keyframes only shape the tween. Retained sounds play the
update as an update, added or source-replaced sounds as their incoming track,
and, when the line stops every BGM sound, those sounds as their outgoing track
(see Audio Channels). Relative pan keyframes continue from the channel pan
before the line, or from the new channel pan when music starts. Channel-level
volume and pan keyframes are flattened into each rendered sound while
preserving that sound's authored or resource-level volume and pan.
Visual items use a flat items array. Each item can set numeric layer to
choose one of the predefined story render layers. Items in the same layer
preserve their array order.
visual:
items:
- id: fog
resourceId: fog
transformId: fullscreen
layer: 30If layer is omitted, the item defaults to 50, matching the previous visual
behavior.
| Layer value | Constant name | Position |
|---|---|---|
10 |
VISUAL_BEHIND_BACKGROUND |
Before background |
20 |
BACKGROUND |
Engine background layer |
30 |
VISUAL_BEHIND_CHARACTER |
Before characters |
40 |
CHARACTER |
Engine character layer |
50 |
VISUAL_BEHIND_DIALOGUE |
Before dialogue |
60 |
DIALOGUE |
Engine dialogue/UI layer |
70 |
VISUAL_BEHIND_CHOICE |
Before choice |
80 |
CHOICE |
Engine choice UI layer |
90 |
VISUAL_FOREGROUND |
Above choice/UI/layouts |
Visual items can use 10, 30, 50, 70, or 90. Layer 90 is still below
screen transitions, overlay stack entries, and confirm dialogs. JavaScript
callers can use the exported RENDER_LAYER, VISUAL_LAYER, and
DEFAULT_VISUAL_LAYER constants when generating project data.
Visual items can contain a layout directly instead of referencing an image,
video, spritesheet, particle, or shared layout resource. Inline layouts use the
same RouteGraphics element format and the same template and resource-reference
resolution as resources.layouts.
A new visual item chooses one render subject:
resourceIdfor an existing image, video, spritesheet, particle, or layout resourcelayoutfor layout elements authored directly on the visual itemtextfor the legacy direct-text form
These fields are mutually exclusive. Prefer layout for new inline content;
it covers text while also allowing containers, rectangles, sprites, input
handlers, and future layout element types.
A plain text visual is a one-element inline layout:
visual:
items:
- id: chapterTitle
layout:
elements:
- id: chapter-title-text
type: text
content: "Chapter 1"
textStyleId: title
width: 720
transformId: titleTop
layer: 70
alpha: 0.9
animations:
resourceId: titleFadeInOne visual can group multiple layout elements under a stable visual container:
visual:
items:
- id: locationCard
layout:
elements:
- id: card-background
type: rect
width: 640
height: 180
colorId: cardBackground
- id: location-icon
type: sprite
imageId: mapPin
x: 70
y: 90
anchorX: 0.5
anchorY: 0.5
- id: location-name
type: text
content: "Old Town"
textStyleId: locationTitle
x: 380
y: 90
anchorX: 0.5
anchorY: 0.5
transformId: locationCardPosition
layer: 70imageId, textStyleId, and colorId are resolved from the normal project
resource collections. Raw renderer fields such as sprite src, text
textStyle, and rectangle fill should not be authored directly. The only
exception is the narrow inline textStyle on a content run described in
Inline Text Run Styles.
Inline visual layouts receive the same template data as shared visual layouts. They can also contain normal layout input handlers:
visual:
items:
- id: scoreCard
layout:
elements:
- id: score-label
type: text
content: "SCORE ${variables.score}"
textStyleId: score
- id: increment-button
type: rect
y: 80
width: 240
height: 72
colorId: button
click:
payload:
actions:
updateVariable:
id: incrementScore
operations:
- variableId: score
op: increment
value: 1
transformId: scoreCardPositionA visual can use a shared transform resource:
- id: chapterTitle
layout:
elements:
- id: title
type: text
content: "Chapter 1"
textStyleId: title
transformId: titleTopOr it can own an inline transform without creating a transform resource:
- id: chapterTitle
layout:
elements:
- id: title
type: text
content: "Chapter 1"
textStyleId: title
transform:
x: 960
y: 180
anchorX: 0.5
anchorY: 0.5
scaleX: 1
scaleY: 1
flipX: true
flipY: false
rotation: 0transformId and transform are mutually exclusive. Existing top-level
transform fields remain supported as overrides for compatibility.
A complete inline layout reauthors the visual subject and creates a fresh
instance. Include every non-default transform, layer, appearance, and animation
field required by that instance:
visual:
items:
- id: chapterTitle
layout:
elements:
- id: title
type: text
content: "Chapter 2"
textStyleId: title
transform:
x: 960
y: 220
anchorX: 0.5
anchorY: 0.5
scaleX: 1
scaleY: 1
rotation: 0
layer: 70To patch the current inline-layout instance, omit layout. Inline transform
patches then merge by field, and appearance-only or animation-only updates do
not need to repeat the subject:
visual:
items:
- id: chapterTitle
transform:
y: 240
alpha: 0.5
animations:
resourceId: titleFadeOutThe existing text form remains supported for compatibility. New projects
should prefer an inline layout so the visual can grow beyond one text element.
For a new legacy text visual, both text.content and text.textStyleId are
required. A later action containing both fields reauthors the subject and
creates a fresh instance; a partial text patch can supply either field alone
and preserves the other instance fields.
visual:
items:
- id: chapterTitle
text:
content: "Chapter 1"
textStyleId: title
transformId: titleTopParticle effects are authored under resources.particles and selected from a
normal visual item with resourceId. A particle resource owns its simulation
area and Route Graphics particle configuration; the visual item owns placement,
layer, alpha, blur, and animations.
Structured particle modules are the preferred format:
resources:
particles:
fireflies:
width: 640
height: 360
seed: 42
modules:
emission:
mode: burst
burstCount: 20
particleLifetime: 30
source:
kind: rect
data:
x: 0
y: 0
width: 640
height: 360
appearance:
texture:
shape: circle
radius: 5
color: "#FFFFFF"
visual:
items:
- id: fireflies
resourceId: fireflies
transformId: particleArea
layer: 50
alpha: 0.8Particle textures can reference project images. Use an image resource ID as a
texture string, or use imageId on an item in a structured texture selection;
the engine resolves it to the image resource's fileId. Generated circle,
ellipse, and rect texture shapes pass through unchanged. Legacy Route
Graphics particle resources with texture, behaviors, and emitter are also
supported.
resources.transforms can define x, y, anchorX, anchorY, scaleX,
scaleY, flipX, flipY, rotation, originX, and originY. x and y use renderer pixels,
anchors are normalized unitless values, scale fields are multipliers, and
rotation is degrees. Character and visual items can override any of those
transform fields for a single item. originX and originY are passed through
to the renderer as the transform origin fields. New character sprite items still
require transformId; after an item exists, a later line can provide only id
and transform fields to patch that item without restating its sprites or visual
resource.
Background actions can also set x, y, anchorX, anchorY, scaleX,
scaleY, flipX, flipY, rotation, originX, and originY at the top level. These fields
can override selected values from transformId, or position the background
without any transformId. Image, video, and spritesheet backgrounds default to
centered placement, computed as x = screen.width / 2 and
y = screen.height / 2, with anchorX: 0.5, anchorY: 0.5, rotation: 0,
scaleX: 1, and scaleY: 1. Layout backgrounds use top-left defaults when a
background transform is authored.
When flipX or flipY is true, the corresponding resolved scale is
multiplied by -1. An omitted scale defaults to 1 for the flip, so
flipX: true resolves to scaleX: -1. Item-level flip values override shared
transform values; explicitly setting a flip to false disables an inherited
flip. A negative authored scale combined with a flip becomes positive.
Background transforms support three authoring modes:
| Mode | Behavior |
|---|---|
transformId only |
Use the complete transform resource |
transformId plus inline fields |
Start from the transform resource, then override the provided fields |
| inline fields without transformId | Apply the provided fields over the background type's default transform |
background:
resourceId: bg_school
transformId: fullscreen
background:
resourceId: bg_school
transformId: fullscreen
scaleX: 1.1
scaleY: 1.1
background:
resourceId: bg_school
x: 960
y: 540
anchorX: 0.5
anchorY: 0.5
rotation: 0
scaleX: 1
scaleY: 1
originX: 960
originY: 540character:
items:
- id: lead
transformId: characterCenter
x: 920
y: 980
anchorX: 0.5
anchorY: 1
scaleX: 0.8
scaleY: 0.9
rotation: 12
originX: 64
originY: 128
sprites:
- id: body
resourceId: leadBody
visual:
items:
- id: fog
resourceId: fog
transformId: fullscreen
layer: 30
anchorX: 0
anchorY: 0
scaleX: 1.2
scaleY: 1.3
rotation: -8
originX: 20
originY: 40Character and visual items can set static alpha, blur, and filters.
alpha accepts values from 0 to 1 and is the preferred field for new
projects. blur uses the same shape as background.blur and screen.blur;
blur: null clears the item blur. filters uses the inline RouteGraphics shader
filter interface; filters: [] clears item filters.
For backward compatibility, the engine also accepts the legacy opacity alias
wherever top-level appearance alpha is accepted: screen and background
actions, character and visual items, and layout elements. When both fields are
present, alpha wins. New projects should use alpha; opacity remains only
so existing projects continue to load. This alias does not apply to visual
fields nested inside hover, click, or rightClick interaction metadata.
Character item appearance applies to the whole character container, so every sprite part is faded, blurred, or filtered together. Visual item appearance applies to the single visual item container, sprite, video, animated sprite, particle system, layout, or text element.
Appearance persists when its presentation channel is omitted or when a later
item action omits the subject. Explicitly authoring a background resourceId,
a non-empty character sprites array, a visual resourceId, a complete visual
layout, or complete visual text creates a fresh instance from that action
alone. Omitted appearance, placement, layer, playback, and animation fields
then use their defaults, even if the subject value is unchanged. See
Presentation Subject Replacement Semantics
for the complete contract and migration notes.
Deprecation note: Subject-less continuation patches such as a background action containing only
opacityremain supported for compatibility in v1.46.0, but support is planned for removal. New content should omit an unchanged presentation channel or emit its complete subject-bearing action.
character:
items:
- id: lead
transformId: characterCenter
alpha: 0.72
blur:
x: 6
y: 9
quality: 3
kernelSize: 9
repeatEdgePixels: true
sprites:
- id: body
resourceId: leadBody
- id: face
resourceId: leadSmile
visual:
items:
- id: fog
resourceId: fog
transformId: fullscreen
layer: 30
alpha: 0.45
blur:
x: 6
y: 9
quality: 3
kernelSize: 9
repeatEdgePixels: true
- id: vignette
resourceId: vignette
transformId: fullscreen
layer: 90
alpha: 0.8Forms are blocking presentation actions for Route Graphics input elements.
Edits stay in transient form drafts until the user submits a valid form. On a
valid submit, the engine commits every field to its configured variable, then
runs the authored submitActions.
The layout should bind buttons to prepared form action batches, matching the confirm-dialog style:
click:
payload:
actions: ${form.submitActions}Inputs opt into a field by name:
- id: name-input
type: input
field: nameStory action:
form:
resourceId: profileForm
fields:
name:
variableId: playerName
required: true
trim: true
placeholder: Name
code:
variableId: playerCode
required: true
submitActions:
nextLine: {}
cancelActions:
rollbackByOffset: {}Use dialogue.character.name for new authored content. dialogue.characterName is still accepted, but only as a compatibility alias for older content and tools, so it is intentionally omitted from the public action summary above.
Character resources can bind their default display name to a string variable:
resources:
variables:
playerName:
type: string
scope: context
default: Guest
characters:
protagonist:
name: Protagonist
nameVariableId: playerNameWhen a dialogue line uses characterId: protagonist, ${dialogue.character.name}
resolves to the current variables.playerName value. The resource name
remains the fallback/editor label.
Preferred authored shape:
dialogue:
characterId: alice
character:
name: Alias
sprite:
transformId: dialoguePortraitLeft
items:
- id: base
resourceId: aliceBody
- id: face
resourceId: aliceSmile
animations:
resourceId: portraitIn
persistCharacter: true
content:
- text: HelloField semantics:
characterIdis the speaker identity. It selects the character resource and its default display name.resources.characters[*].nameVariableIdbinds the resource display name to a string variable.character.nameis only a display-name override.character.spriteis an optional layered speaker sprite group rendered with the dialogue action.persistCharacter: truemeans later dialogue lines that omit speaker fields reuse the previouscharacterIdandcharacter.nameoverride.persistSprite: truekeepscharacter.spritewhen later dialogue lines omit a sprite, independently from speaker persistence and speaker changes.persistSprite: falseclears the previous sprite on the next dialogue line that omits a sprite. For backward compatibility,persistCharacter: truecontinues to persist sprites whenpersistSpritehas never been specified and later lines inherit the speaker. An explicitcharacterIdkeeps the legacy behavior of clearing that sprite unlesspersistSprite: truewas established.- If a later dialogue line explicitly provides
characterIdwithoutcharacter.nameorcharacter.sprite, the previous override is cleared and the displayed name falls back to the character resource name. - If a later dialogue line omits
characterIdbut providescharacter.nameorcharacter.spritewhilepersistCharacteris active, the provided fields update the persisted speaker and omitted fields keep their previous values.
runtime.dialogueTextSpeed is the persisted user preference. A dialogue action
can provide textSpeed to override that preference for the authored line only:
dialogue:
textSpeed: 12
content:
- text: "This line reveals slowly."Dialogue layouts should bind the effective dialogue value to the Route Graphics
text-revealing.speed field:
- id: dialogue-text
type: text-revealing
content: ${dialogue.content}
speed: ${dialogue.textSpeed}
revealEffect: typewriterIf the action omits textSpeed, ${dialogue.textSpeed} resolves to
${runtime.dialogueTextSpeed}. The override does not mutate the runtime
preference and is cleared by the next dialogue action that omits it.
runtime.autoForwardSpeed is the persisted user preference for global auto
mode reading speed. It uses the same 0 to 100 settings range as the volume
controls and defaults to 50. A value of 0 is half speed, 50 is normal,
and 100 is twice as fast. Update it with the same runtime-action pattern used
by dialogue speed and audio volume controls:
setAutoForwardSpeed:
value: 100The speed setting is converted exponentially across the half-speed to
double-speed range and applies only to the length-derived reading time. The
persisted runtime.autoForwardDelay minimum is still added unchanged, subject
to the auto-mode delay cap.
The speed multiplier is 2 ** x, where x = (speed - 50) / 50 is evaluated in
ordinary double arithmetic and 2 raised to that double x is then rounded to
the nearest double (correctly rounded). It is the same on every JavaScript host;
the host's Math.pow differs in the last bit between engines and C libraries.
Reading units always use the bundled Unicode 17.0 grapheme segmenter and Unicode
17.0 Han, Hiragana, Katakana, and Hangul code point ranges, rather than the
host's Intl.Segmenter and regular-expression script data, whose Unicode
version depends on the browser or runtime. The text is not normalized first,
because String.prototype.normalize also uses the host's Unicode data.
In ADV mode, dialogue.append: true appends the line content to the current
dialogue content instead of replacing it. The engine exposes
dialogue.initialRevealedCharacters to dialogue layouts so a text-revealing
element can keep the existing prefix visible and reveal only the appended suffix.
When an append action omits speaker fields, the current speaker is kept for the
continuation; explicit characterId or character fields still update it.
dialogue:
ui:
resourceId: advDialogue
content:
- text: "Held prefix: "
dialogue:
append: true
content:
- text: "continuing from the same visible line."The dialogue layout should pass the template value through to Route Graphics:
- id: dialogue-text
type: text-revealing
content: ${dialogue.content}
initialRevealedCharacters: ${dialogue.initialRevealedCharacters}
speed: ${dialogue.textSpeed}
revealEffect: typewriterdialogue.character.sprite renders one dialogue speaker sprite group. It uses a
single transform and animation selection for all sprite layers.
dialogue:
characterId: alice
character:
sprite:
transformId: dialoguePortraitLeft
items:
- id: base
resourceId: aliceBody
- id: face
resourceId: aliceSmile
animations:
resourceId: portraitIn
playback:
continuity: render
persistSprite: true
content:
- text: HelloRuntime behavior:
sprite.transformIdresolves throughresources.transforms.sprite.items[].resourceIdresolves throughresources.images.sprite.animations.resourceIdresolves throughresources.animations.- The rendered container id is
dialogue-character-sprite. - Sprite layer ids are
dialogue-character-sprite-${item.id}. - The sprite group is added after the dialogue UI layout elements, so the portrait can render above the UI when they overlap.
persistSpriteis independent frompersistCharacter, so a persisted portrait can remain visible when a later line changes the speaker.- An animation-only
character.spritepayload can animate out the previous dialogue sprite group:
dialogue:
character:
sprite:
animations:
resourceId: portraitOut
content:
- text: GoodbyeExamples:
# Omitted speaker fields keep the persisted alias.
- dialogue:
characterId: alice
character:
name: Alias
persistCharacter: true
content:
- text: Hello
- dialogue:
content:
- text: Hi againThe second line still displays Alias.
# Explicit characterId resets the speaker to the resource name
# unless character.name is provided again.
- dialogue:
characterId: alice
character:
name: Alias
persistCharacter: true
content:
- text: Hello
- dialogue:
characterId: alice
content:
- text: Hi againThe second line displays Alice, not Alias.
# The portrait remains when a later line changes the speaker.
- dialogue:
characterId: alice
character:
sprite:
transformId: dialoguePortraitLeft
items:
- id: base
resourceId: aliceBody
persistSprite: true
content:
- text: Hello
- dialogue:
characterId: bob
content:
- text: I am speaking while the previous portrait remains visible.Set persistSprite: false on a later dialogue line without a sprite to remove
the previously persisted sprite on that line.
Template/runtime paths:
- Active dialogue layouts should use
${dialogue.character.name}. - Active dialogue layouts can inspect sprite metadata at paths such as
${dialogue.character.sprite.items[0].resourceId}. - NVL line-item layouts should prefer
${line.character.name}.${line.characterName}remains available as a compatibility alias. - NVL line-item layouts can inspect sprite metadata at
${line.character.sprite}. - Dialogue history layouts should prefer
${item.character.name}.${item.characterName}remains available as a compatibility alias. - Dialogue history layouts can inspect sprite metadata at
${item.character.sprite}.
Engine audio is authored with sounds and rendered as Route Graphics
audio-channel.children.
- BGM owns one persistent channel and may contain many sounds.
- Voice owns one line-scoped channel and may contain many sounds.
- SFX may contain any number of single-line or persistent channels, each with many sounds.
- Channel and sound IDs are namespaced in render state so authored IDs remain stable and globally unique.
- Canonical sound IDs must be unique within their channel, and canonical SFX channel IDs must be unique within the SFX action.
- BGM
audioEffectsupdates animate one track per sound, like the visual update fallback inAnimationModel.md, preferring the sounds the line plays. A retained sound plays the update as an update. An added or source-replaced sound plays it as its incoming (enter) track, and its final keyframes must still match the sound's persistent mix. When the line stops every sound, each one plays the update as its outgoing (exit) track, so an update that fades to 0 fades the music out. A sound removed while others play or enter stops without an effect; if those others do not change, the update has no effect at all. Transitions target only added, removed, or source-replaced sounds. A sound that keeps playing from the same source plays no transition and takes its new mix at once, including the finalnextvalues that the transition writes into the persistent mix. Whether a line keeps a sound depends on how the player reached it (the previous line, a section transition, or a reset), so the same line can start the music with the transition on one path and keep it on another.
actions:
bgm:
volume: 80
pan: -0.2
loop: true
sounds:
- id: theme
resourceId: music_1
volume: 90
muted: false
pan: 0.1
playbackRate: 1.1
startAt: 5
endAt: 30
- id: ambience
resourceId: forest_ambience
volume: 40
voice:
sounds:
- id: alice
resourceId: alice_001
- id: narrator
resourceId: narrator_001
startDelayMs: 250
sfx:
channels:
- id: ui
volume: 80
sounds:
- id: confirm
resourceId: ui_confirm
- id: environment
pan: 0.5
loop: true
applyMode: persistent
sounds:
- id: rain
resourceId: rainEach canonical sound supports loop, volume, muted, pan,
startDelayMs, playbackRate, startAt, and endAt. startDelayMs is in
milliseconds; startAt and endAt are offsets in seconds for partial source
playback. An action sound overrides defaults from its sound or Voice resource,
and endAt: null explicitly clears a resource end offset. After those defaults
and overrides are resolved, a non-null endAt must be greater than or equal to
startAt. With canonical bgm.sounds, top-level bgm.loop repeats the complete
scheduled sound sequence after every sound finishes. Canonical Voice channels
use top-level voice.loop the same way, and each canonical SFX channel supports
loop. A looping channel cannot contain a sound whose own loop is true.
startDelayMs remains a sound-level setting. For legacy single-sound BGM and
Voice actions, top-level loop and startDelayMs continue to configure that
sound.
Omitting bgm preserves its current desired channel state. bgm.sounds: []
stops the BGM channel. Voice channels and SFX channels whose applyMode is
singleLine are cleared when the next line omits their action. singleLine is
the default SFX apply mode. A persistent SFX channel remains active across
following lines until an action replaces its ID, provides that ID with
sounds: [], provides sfx.channels: [] to stop every SFX channel, or uses
cleanAll.
The previous single-sound forms remain compatibility shorthands:
bgm: { resourceId: music_1, volume: 80 }
voice: { resourceId: alice_001, volume: 80 }
sfx:
items:
- { id: door, resourceId: door_close, volume: 60 }They compile respectively to the BGM channel, Voice channel, and a default SFX channel.
Legacy BGM has no authored clip ID, so its runtime sound ID is derived from
resourceId: bgm:music_1 in this example. It matches a canonical BGM sound
whose id is music_1. Volume-only changes between those forms retain the
playing sound, provided the source and playback timing remain the same. ID
components escape % and : just like canonical sound IDs.
This fallback is runtime-only; it does not rewrite project data or save data.
For compatibility with older editor conversions, a single canonical clip with
ID default also uses its resourceId as its runtime identity. Other explicit
IDs remain authoritative, including default in multi-clip channels. Separate
occurrences must retain distinct IDs. A changed source, playback range, or start
delay can still restart playback.
Voice and SFX identity rules are unchanged.
Canonical channel audio uses three multiplicative layers:
- authored channel volume;
- runtime
musicVolumefor BGM orsoundVolumefor Voice/SFX; - authored or resource-level sound volume.
The effective output is:
(channelVolume * runtimeVolume * soundVolume) / 10000;Each omitted authored volume defaults to 100. Runtime volume preferences use
the project runtime default when configured, otherwise the engine default.
Persisted device preferences take precedence over both. runtime.muteAll emits
muted: true on every channel without destroying its configured volume. A
channel's authored muted: true is combined with the runtime mute.
Legacy single-sound BGM and Voice payloads preserve their previous effective volume behavior when compiled into channels.
Videos cannot author a resource volume: resources.videos accepts only
fileId, fileType, width, and height. Their audio follows the runtime
channel selected by where the video is rendered:
- background videos (
background.resourceId) followruntime.musicVolume; - visual videos (
visual.items[].resourceId) followruntime.soundVolume.
Both runtime volume preferences default to 50, preserving the previous video
output of 50. Changing either setting updates its playing videos immediately.
runtime.muteAll renders both video kinds at volume 0 while playback
continues; unmuting restores each channel's current setting without restarting
the video.
Voice assets are grouped by scene under resources.voices. Every item in
voice.sounds stores a scene-local resourceId; the engine resolves the scene
from the current section. The Voice channel uses runtime.soundVolume as its
runtime volume layer.
resources:
voices:
scene_intro:
alice_001:
fileId: voices/scene_intro/alice_001.ogg
volume: 90
pan: -0.1
playbackRate: 1
startAt: 0
endAt: null
story:
scenes:
scene_intro:
sections:
opening:
lines:
- id: line_001
actions:
dialogue:
content:
- text: "You're late."
voice:
volume: 80
sounds:
- id: alice
resourceId: alice_001All layout-backed UI surfaces render against the same base template-data contract:
- dialogue UI layouts
- generic
layoutpresentation layouts controllayoutschoicelayouts- overlays
- confirm dialogs
- background/CG layouts that render layout elements
- layout-backed visual items
Shared template roots:
variablesimageGallery(the computed singleton projection, ornullwhen absent)musicRoom(the computed singleton projection, ornullwhen absent)sceneReplay(the computed singleton projection, ornullwhen absent)runtimesaveSlotscharactersisChoiceVisibleisFormVisiblecanRollback
Roots with special presence semantics:
dialogueanddialogueLinesare added only when active dialogue template data existschoiceis added by the choice-layout render path when a choice is activeformis added by the form-layout render path when a form is activehistoryDialogueis always present in the shared template data and defaults to[]. It contains chronological dialogue from the active context across section boundaries and follows save/load, rollback, branching, and story-reset semantics.confirmDialogis always present in the shared template data and isundefinedwhen no confirm dialog is active
The dialogue omission is intentional. When no active dialogue state exists, the
runtime does not materialize dialogue: {} or dialogueLines: []. Existing
truthiness checks such as $if dialogue therefore keep their previous
behavior.
Dialogue template shape:
dialogue:
characterId: alice
persistCharacter: true
character:
name: Alias
sprite:
transformId: dialoguePortraitLeft
items:
- id: base
resourceId: aliceBody
- id: face
resourceId: aliceSmile
content:
- text: Hello
textSpeed: 12
lines:
- characterId: alice
character:
name: Alice
sprite:
transformId: dialoguePortraitLeft
items:
- id: base
resourceId: aliceBody
characterName: Alice
textSpeed: 12
content:
- text: HelloCompatibility notes:
dialogue.characterIdis available anywhere the shared layout template data is used, not only inside the dedicated dialogue UI.dialogue.character.nameremains the preferred speaker display-name path.dialogue.character.sprite,line.character.sprite, anditem.character.spriteexpose the authored dialogue sprite metadata.dialogueLinesremains a compatibility alias fordialogue.lines.line.characterNameremains a compatibility alias forline.character.name.- Existing authored templates that rely on
line.characterNameordialogueLinesdo not need to change.
Examples:
# Generic layout or control condition
$when: 'dialogue.characterId == "alice"'# Preferred display-name path
content: "${dialogue.character.name}"# Compatibility alias still supported
content: "${dialogueLines[0].characterName}"