StoryScript (Name not final) is a game-development scripting language and toolchain for prototyping story-driven games, especially visual novels.
This repository includes:
- A language specification and working examples.
- A Rust parser/validator CLI.
- A Rust terminal-based player (TUI) to run StoryScript scenes.
- A VS Code extension for
.StoryScriptsyntax highlighting and language intelligence. - A Flutter plugin integration for the Rust runtime.
Live demo: web
.
├── PLAN.md # Language spec and validation rules
├── example/ # Example StoryScript files
├── parser/rust/ # storyscript-parser (lexer/parser/validator)
├── player/ # storyscript-player (TUI runtime)
├── storyscript_player_core/ # Flutter plugin integration and WebAssembly bindings
└── tool/vscode-storyscript/ # VS Code language extension
- Rust toolchain (stable)
- Node.js + npm (for VS Code extension packaging)
- Flutter SDK (optional, for Flutter plugin and Web integration)
cd parser/rust
cargo run -- ../../example/the_last_signal.StoryScriptOptional JSON diagnostics output:
cargo run -- ../../example/the_last_signal.StoryScript --jsoncd player
cargo run -- ../example/the_last_signal.StoryScriptIf no file is passed, the player scans for .StoryScript files in the current directory and ../example.
Up/Down: navigate chooser or scroll story viewEnter: continue dialogue/narration or open selected story file1-9: choose choice optionsQ: quit player view (or exit app from chooser)
StoryScript uses:
* INITfor global state, actors, and@start- Root-only child manifest via
@include [ ... ]inside* INIT #PREPfor state mutation and engine directives (@bg,@bgm,@sfx)#STORYfor narration, dialogue, branching (if/else if/else), transitions, and standalone variable output ($var)- Snapshot
forandrepeatloops in both#PREPand#STORY, withbreak/continuenearest-loop behavior - Nested
@choiceentry groups usingif,repeat, andfor ($item in snapshot $array)(runtime-expanded choice cap: 9 options) - Numeric expressions with
+,-,*,/,%(modulo is integer-only) - Built-ins:
abs(x),rand(),rand(min, max),pick(array),pick(count, array),array_push,array_pop,array_strip,array_clear,array_contains,array_size,array_join,array_get,array_insert,array_remove - Top-level user logic declarations:
logic name($param as type, ...) { ... }with optional typed return-> type ${var}inline interpolation in string literals across all phases (\$for literal dollar)- Typed declarations in
* INITusingas integer|string|boolean|decimal|array<...> - Typed local declarations in
#PREPusing the same shape:$name as <type> = <expr> - Variable type is immutable after declaration
Local scope notes:
- Local declarations are allowed only in
#PREP - Locals are visible in both
#PREPand#STORYof the same scene - Locals are reset every time that scene is entered/re-entered
- Local names cannot collide with globals declared in
* INIT
Module include notes:
- Only root
* INITcan use@include [ ... ] - Included child files must not define
* INIT - Included child files must define exactly one
* REQUIRE { ... } - Include paths are resolved relative to the root script file
Built-in notes:
rand()andrand(min, max)are assignment-target driven:- integer target -> integer random
- decimal target -> decimal random
rand(min, max)is inclusive.- decimal assignment allows integer/decimal bounds for
rand(min, max). - decimal assignment allows integer/decimal candidates for
pick(array)and decimal array probes inarray_contains. - mutating array functions (
array_push,array_strip,array_clear,array_insert) are valid in#PREPstatement form and forbidden in#STORY. - collection scalar arguments (
value,index,count,string_separator) must be literal or$variable.
Minimal example:
* INIT {
$morale as integer = 50
$captain_name as string = "Ari"
$threat_level as decimal = 1.5
$alerted as boolean = false
@actor CMDR "Commander"
@start opening
}
* opening {
#PREP
@bg "bridge.png"
#STORY
"The bridge lights flicker."
CMDR: "Status report. Morale=${morale}"
$morale
@choice {
"Investigate the signal" -> signal_room
"Lock down the deck" -> lockdown
}
}
For full syntax and validation rules, see PLAN.md.
Minimal modular include example:
// main.StoryScript
* INIT {
$system_stability as integer = 100
$has_admin_key as boolean = true
@actor TEO "Teona" {
focus -> "teo_focus.png"
}
@actor GIP "Gippie" {
alert -> "gip_alert.png"
}
@include ["modules/minigame_hack.StoryScript"];
@start hack_sequence_start;
}
// modules/minigame_hack.StoryScript
* REQUIRE {
$system_stability as integer;
$has_admin_key as boolean;
@actor TEO [ focus ];
@actor GIP [ alert ];
}
* hack_sequence_start {
#PREP
$system_stability = $system_stability - 15
#STORY
GIP(alert, Center): "Warning! Countermeasures active."
@end;
}
Path: tool/vscode-storyscript
The extension provides syntax highlighting, language configuration, and an LSP server for .StoryScript files.
From the extension folder:
cd tool/vscode-storyscript
npm install
npm run compile
npx vsce packageThat creates a .vsix file in the same folder.
code --install-extension storyscript-syntax-0.2.0.vsixYou can also open the extension folder in VS Code and use the Extensions UI to install from VSIX.
- Parser and player crates compile successfully with
cargo check. - VS Code extension now includes syntax highlighting plus LSP-based diagnostics, completion, hover, document symbols, definition, and references for
.StoryScriptfiles.
This repository includes a Flutter plugin wrapper around the Rust runtime at storyscript_player_core. It supports WebAssembly bindings for Flutter Web as well as native mobile targets.
cd storyscript_player_core
flutter_rust_bridge_codegen build-web --wasm-pack-rustflags "-Ctarget-feature=+atomics -Clink-args=--shared-memory -Clink-args=--max-memory=1073741824 -Clink-args=--import-memory -Clink-args=--export=__wasm_init_tls -Clink-args=--export=__tls_size -Clink-args=--export=__tls_align -Clink-args=--export=__tls_base"This generates the WebAssembly artifacts required for Flutter Web when using flutter_rust_bridge.
cd storyscript_player_core
flutter run --web-header=Cross-Origin-Opener-Policy=same-origin --web-header=Cross-Origin-Embedder-Policy=require-corpThese headers are necessary for shared-memory WebAssembly builds and allow the web app to load the generated Wasm module correctly.
For Android and iOS native builds, install the required Rust targets:
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-iosThen build the Android library with cargo ndk:
cargo ndk -t armeabi-v7a -t arm64-v8a -t x86 -t x86_64 -o ../android/app/src/main/jniLibs build --releaseNote: Ensure you have Flutter installed and that
wasm-packis available in your PATH before running these commands.
See LICENSE.

