Godot
The pure GDScript runtime. No native extension to compile, no web view: it loads a .storyletsc bundle and deals from it directly, held to the same shared test suite as every other engine.
Requires Godot 4.x; verified on 4.7. The runtime uses only plain GDScript, so it also runs headless.
Install
Section titled “Install”Download the Godot zip from the download page, drop the storyletengine/
folder into your project’s addons/ directory and enable the plugin in Project ▸ Project
Settings ▸ Plugins.
The runtime works with or without the plugin enabled. Enabling it registers the .storyletsc
importer (so bundles become assets you can load()), the bundle inspector, and the export
hook that keeps your bundle in an exported build (see Exporting your game).
Load a bundle
Section titled “Load a bundle”Either read the file yourself:
var text := FileAccess.get_file_as_string("res://story.storyletsc")var loaded := StoryletBundle.load_from_string(text)if not loaded["ok"]: push_error(loaded["error"]) returnvar bundle = loaded["bundle"]Or, with the plugin enabled, load("res://story.storyletsc") gives you a
StoryletBundleResource carrying json_text and get_bundle().
Build an engine, open a flow
Section titled “Build an engine, open a flow”var engine := StoryletEngine.create(bundle, {"seed": 7, "log": true})var flow := engine.open_flow("main")StoryletDebug.register(engine, "main") # optional: lets the state panel find itThe engine is the world; every play call lives on a flow - one playthrough - opened by
name. A single-player game opens "main" and never thinks about it again; several flows run
parallel playthroughs over the same shared state (the sharing rules).
The same seed always deals the same cards. "log": true keeps the event logs: flow.log()
is that flow’s own and engine.log() is the RUN’s, every flow’s events in one order with
each entry naming its flow. That last one is the only place a story action in another flow
moving shared state is visible, and the state panel shows both (capped at 1000; {"cap": n}
sets your own). An unknown key in the options Dictionary is an error, so a typo tells you
instead of doing nothing.
Deal, peek, outcomes, play
Section titled “Deal, peek, outcomes, play”flow.deal_many() # refresh every handvar board := flow.board() # hand gameId -> Array of card viewsfor hand in board: for card in board[hand]: print("%s holds %s" % [hand, card["gameId"]])
var looks := flow.peek("village", {"area": "forest"}, 3) # look, don't deal
for outcome in flow.outcomes(card_id, hand_id): # ask when you show them if outcome["available"]: var err := flow.play(card_id, outcome["gameId"], hand_id)Card views and outcome views are Dictionaries: id, gameId, title, purpose and
fields on a card; available on an outcome. play() returns an error String, empty on
success, and changes nothing if the outcome is gated shut or the card isn’t in that hand.
Your game’s state
Section titled “Your game’s state”flow.set_property("world.time_of_day", "night") # write before you dealflow.get_property("story.reputation")flow.list_properties() # every declared property
flow.advance_turns("village", 1.0)var turn := flow.turn("village")var boxes := flow.list_boxes()The paths, and when to write them: Your game’s state.
Save and load
Section titled “Save and load”var envelope := engine.save_game()var load_err := engine.load_game(envelope) # rebuilds every flow...flow = engine.get_flow("main") # ...so re-take your handlesStoryletSave.serialize_state(engine, world_values) and
StoryletSave.deserialize_state(engine, text) (which hands back the file’s @world values
for your game to apply - why the engine never saves them) are
the .storyletsave string boundary. A foreign, malformed or wrong-project blob is refused, so
a bad file can’t corrupt a run.
Errors
Section titled “Errors”GDScript has no exceptions, so the addon reports errors as values:
play(),load()andset_property()return an error String, empty on success.- Bad references and bad option Dictionaries
push_error. - An unknown box on
board(box_ref)is refused withpush_errorand an empty Dictionary. - An evaluation error inside a deal or peek makes that card or deck unavailable and puts a diagnostic in the trace, exactly as the reference runtime does. Never a silent pass, never a crash.
The other small differences from the other runtimes are listed on Compatibility.
The state panel
Section titled “The state panel”Add a StoryletStatePanel to your scene. It’s an in-game overlay, debug_only by default, so
it builds nothing in a release export and is safe to leave in a scene that ships. With an
engine registered through StoryletDebug, it saves and loads .storyletsave files with
Save State… / Load State… and shows the run log (every flow’s events in one order),
then a section per open flow: that flow’s declared properties (with a filter, editable), its
per-box turns, its board and its own retained log, each log behind per-kind filters with
Autoscroll, Copy and Clear. It reads the flows off the engine, so one
registration covers every flow, however many you open later.
To watch the game from Storyletter instead, and to have saves reach the run without a
restart, add a StoryletLiveLink node and attach your ENGINE (the link finds your flows
itself); it opens only in a debug
build. Wiring and the protocol: Live Link.
The bundle inspector
Section titled “The bundle inspector”Select an imported bundle in the FileSystem dock and the Inspector shows what the bundle offers your code: hands, boxes, tags, declared properties. Nothing running needed. See the bundle inspector.
Exporting your game
Section titled “Exporting your game”With the plugin enabled there’s nothing to configure: the addon’s export hook puts the raw
.storyletsc into the exported build at its original path, so
FileAccess.get_file_as_string("res://story.storyletsc") reads the same bytes in the editor
and in the export, on every platform.
If you run with the plugin disabled, Godot treats a .storyletsc as a non-resource file and
leaves it out of the export. In that case add *.storyletsc to your export preset’s
Resources ▸ “Filters to export non-resource files/folders”.
The demo
Section titled “The demo”addons/storyletengine/demo/board_demo.tscn is the Board demo: the Hamlet bundle dealt
onto a board you can play, with the same hands, control labels and transcript as the other
three runtimes. Open the scene and press Play. The smallest part to read first in
board_demo.gd is building the engine, opening a flow, dealing and reading board(); the rest
is UI. Delete
the folder freely; nothing depends on it.
- What every runtime shares: Dev tools.
- Why it matches the other engines exactly: Compatibility & conformance.
MIT-licensed open source · Made by Ian Thomas · storylet.studio