Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Add Polish

Two problems with Mira so far: she says the same opening line every time, and the scene feels silent and static. Let’s fix both, and add a sound cue so your engine can react to the sale.

Update mira.bub to the complete version:

title: Start
---
<<declare $gold = 8>>

=> Seagull: Squawk!
=> A cart rumbles past with barrels of fish.
=> The salt air stings your eyes.

<<once>>
    Mira: Oh! A new customer. Fresh stock just arrived, as luck would have it.
<<else>>
    Mira: Back again? The price hasn't changed.
<<endonce>>

Mira: Five gold for a health potion. Best deal in the harbour district.
-> Buy a health potion (5 gold) <<if $gold >= 5>>
    <<set $gold = $gold - 5>>
    <<play_sfx "clink">>
    Mira: Pleasure doing business. You've got {$gold} gold left.
-> I can't afford that right now. <<if $gold < 5>>
    Mira: Come back when your pockets are heavier.
-> Just looking.
    Mira: ...You're still just looking?
===

Reload with r. Play through. Then press R (rerun) - Mira now gives her second-visit greeting. Play through again. The ambient line at the top changes each time.

<<once>>

<<once>> runs the first branch exactly once. Every visit after fires the <<else>> branch. No variable needed.

<<once>>
    Mira: Oh! A new customer. Fresh stock just arrived.
<<else>>
    Mira: Back again? The price hasn't changed.
<<endonce>>

The “once-seen” state is saved with the runner and survives save/load. Press r (full reload) to reset it, R (rerun) to keep it.

You can also make a block that runs once and goes completely silent after:

<<once>>
    Narrator: Something shifts in the air as you approach.
<<endonce>>

See Once Blocks for the full forms, including <<once if condition>>.

Line groups (=>)

The three => lines at the top are a line group. Every time the runner reaches them, it picks one. With the default strategy, it picks whichever one you’ve seen least recently - so you never hear the same ambient line twice in a row.

=> Seagull: Squawk!
=> A cart rumbles past with barrels of fish.
=> The salt air stings your eyes.

Your game receives a normal DialogueEvent::Line for whichever one got picked. It doesn’t know there were three variants - it just gets the chosen one.

Tell the runner which strategy to use. BestLeastRecentlyViewed is the right pick for ambient barks:

use bubbles::saliency::BestLeastRecentlyViewed;

runner.set_saliency(BestLeastRecentlyViewed::new());

See Line Groups for guarded variants and mixing in tags.

Commands: talking to your engine

<<play_sfx "clink">> is a command. Bubbles doesn’t know what it means - it just emits a DialogueEvent::Command with the name and arguments, and your game handles it.

DialogueEvent::Command { name, args, .. } => {
    match name.as_str() {
        "play_sfx" => audio.play_one_shot(&args[0]),
        _ => {}
    }
}

Commands are the bridge between your dialogue and your engine. Some common patterns:

Audio cues:

<<play_sfx "door_creak">>
<<play_music "tavern_night">>
<<stop_music>>

Voice-over by line id. Tag a line with #line:some_id and use line_id in the event to look up the VO clip:

Mira: Fresh stock just arrived. #line:mira_greet_first
DialogueEvent::Line { line_id, .. } => {
    if let Some(id) = &line_id {
        audio.play_voice(id); // "mira_greet_first"
    }
}

Per-line metadata with tags. Tags travel with every Line event and are yours to interpret:

Mira: Five gold each. #portrait_mood angry #subtitle_style bold
DialogueEvent::Line { tags, .. } => {
    for tag in &tags {
        if let Some(mood) = tag.strip_prefix("portrait_mood ") {
            ui.set_portrait_mood(mood); // "angry"
        }
    }
}

Animations, VFX, game state:

<<trigger_cutscene "mira_winks">>
<<give_item "health_potion">>
<<set_camera_target "mira">>

The rule: if your dialogue needs a value back (a reputation check, an inventory count), use a custom function. If it’s a side effect (play a sound, trigger an animation, give an item), use a command. See Commands and Custom Functions for the full story on each.

What’s next

That’s the tutorial. You’ve got a working NPC with state, variety, first-visit behaviour, and engine signals. The language reference chapters cover everything in depth - head there for the full forms of anything you want to extend.


Next: Nodes and Lines - the full language reference starts here