bubbles
A minimal, engine-agnostic dialogue runtime for Rust games.
Crates.io and docs.rs badges above fill in after the first release on crates.io.
Write branching .bub scripts, compile them once at startup, then drive the
dialogue from any game loop with a simple pull-based event API. Designed to
integrate cleanly into Bevy, Godot, or any custom Rust engine — zero async
primitives, zero allocations in the hot path beyond the events themselves.
Requirements: Rust 1.95 or later (see rust-version in Cargo.toml).
Features
| Feature | Description |
|---|---|
| Nodes | title: / --- / === structure; optional tags: metadata |
| Lines | Plain text, Speaker: attribution, trailing #hashtag metadata |
| Shortcut options | -> with optional <<if cond>> guard and indented body |
| Jumps | <<jump NodeName>> |
| Conditionals | <<if>> / <<elseif>> / <<else>> / <<endif>> |
| Typed variables | Number, Text, Bool; <<set $x = expr>> |
| Expressions | Arithmetic, comparison, boolean, unary, parens, proper precedence |
| Inline substitution | {expr} anywhere in line / option / command text |
| Host commands | <<verb arg1 arg2>> surfaced as DialogueEvent::Command |
| Built-in functions | visited, visited_count, random, random_range, dice, round, floor, ceil, min, max, abs, clamp, string, int |
| Custom functions | Host-registered closures callable inside any expression |
| Once blocks | <<once>> / <<once if cond>> / <<endonce>> with optional <<else>> |
| Detour / return | <<detour Node>> / <<return>> subroutine stack |
| Smart variables | <<declare $x = expr>> initialises once, stored in VariableStorage |
| Line groups | => alternatives selected by the active SaliencyStrategy |
| Node groups | Multiple nodes sharing a title with when: header conditions |
| Saliency strategies | FirstAvailable, RandomAvailable, BestLeastRecentlyViewed, or custom |
| Multi-file compile | compile_many(&[(name, source)]) with duplicate-title error |
| Reference validation | Catches typo'd jump/detour targets at compile time |
| Program introspection | node_titles(), node_tags(), node_exists(), variable_declarations() |
| Localisation seam | LineProvider trait consulted for #line:id-tagged lines |
| Stable line ids | #line:<id> also sets line_id on DialogueEvent::Line / DialogueOption (for VO, analytics); see line_id_from_tags |
| Save / load | RunnerSnapshot (serde feature) captures visits, once-seen, active node |
Quick start
Add to Cargo.toml:
[]
= "0.1"
use ;
Run the included examples:
Script syntax
title: NodeName
tags: optional space-separated-tags
when: <condition expression> # node-group header
---
Speaker: A plain line.
A narrator line. # no speaker
The count is {$count}. # inline expression substitution
<<set $x = $x + 1>> # typed variable assignment
<<declare $hp = 100>> # initialise-once (skips if already set)
<<if $x > 10>>
A conditional line.
<<elseif $x == 5>>
Another branch.
<<else>>
The fallback.
<<endif>>
-> Option text
Lines under the option.
-> Only when rich <<if $gold >= 100>>
You splurge.
<<jump OtherNode>> # jump (clears call stack)
<<detour SubScene>> # call-stack push
<<return>> # pop back to caller
<<once>>
Fires the very first time only.
<<else>>
Fires every subsequent time.
<<endonce>>
=> Line variant A. # line group — one chosen by saliency strategy
=> Line variant B.
=> Line variant C.
<<my_command arg1 arg2>> # host command — surfaced as DialogueEvent::Command
===
Extension points
| Trait / Type | Purpose |
|---|---|
VariableStorage |
Pluggable variable store (default: HashMapStorage) |
SaliencyStrategy |
Line / node group selection policy |
LineProvider |
Localisation lookup for #line:id-tagged lines |
line_id_from_tags |
Same id rule as events: parse #line: from a tag slice (e.g. custom UI) |
FunctionLibrary |
Register host functions callable inside expressions |
Custom saliency
use ;
runner.set_saliency; // maximum variation
runner.set_saliency; // uniform random
Localisation
use HashMapProvider;
let mut provider = new;
provider.insert;
runner.set_provider;
A line tagged #line:greeting_id will be substituted with the provider's
translation before the event is emitted.
Host functions
runner.library_mut.register;
Feature flags
| Flag | Default | Description |
|---|---|---|
rand |
on | Enables random(), random_range(), dice() builtins and RandomAvailable saliency |
serde |
off | Derives Serialize / Deserialize on Value, HashMapStorage, and RunnerSnapshot |
full |
off | Shorthand for rand and serde together (features = ["full"]) |
Save / load
// Capture session state mid-dialogue (requires serde feature).
let snap = runner.snapshot;
let json = to_string?;
// … restore later in a new session …
let snap: RunnerSnapshot = from_str?;
runner.restore?;
RunnerSnapshot preserves visit counts and exhausted <<once>> blocks.
Variable storage is serialised separately via runner.storage().
License
Licensed under either of Apache-2.0 or MIT at your option.