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

Tags and Metadata

Your game needs to know more than just what a line says. It needs to know how to say it: which portrait to show, whether to play a voice-over clip, how to style the subtitle, whether a line should trigger a camera cut. Tags are how you attach that information to a line without hardcoding it into your Rust.

A tag is just a #word at the end of a line, option, or node header. It doesn’t change dialogue behaviour - it travels with the event so your game code can act on it.

Line tags

Aria: Halt! #combat #loud

Those tags arrive on the event:

DialogueEvent::Line { text, tags, .. } => {
    // tags = ["combat", "loud"]
    if tags.iter().any(|t| t == "loud") {
        audio.play_on_bus("sfx_shout", "loud_bus");
    }
}

Some things you can do with this:

  • Trigger audio: #sfx door_creak, #voice_bus ambient
  • Swap a portrait or expression: #portrait angry, #portrait relieved
  • Style the subtitle: #subtitle_style whisper, #subtitle_style urgent
  • Camera direction: #camera closeup, #camera wide
  • Gameplay hooks: #hostile, #quest_complete, #gift
Mira: You won't find better prices in three ports. #portrait smug #sfx coin_jingle
DialogueEvent::Line { tags, .. } => {
    for tag in &tags {
        if let Some(mood) = tag.strip_prefix("portrait ") {
            ui.set_portrait(mood); // "smug"
        }
        if let Some(sfx) = tag.strip_prefix("sfx ") {
            audio.one_shot(sfx); // "coin_jingle"
        }
    }
}

Options take tags the same way:

-> Sneak past. #stealth
-> Charge in! #combat #loud

At runtime those strings show up on each DialogueOption::tags inside DialogueEvent::Options, the same idea as DialogueEvent::Line::tags for spoken lines.

Node tags

Nodes can carry tags in their header:

title: TavernEvening
tags: scene indoor warm
---
Barkeep: Evening.
===

Multiple tags, space-separated. Query them before running to pre-load music, prepare ambient systems, or build an editor scene list:

if let Some(tags) = program.node_tags("TavernEvening") {
    if tags.iter().any(|t| t == "indoor") {
        audio.load_ambience("room_tone");
    }
}

Node tags also surface via node_titles and related introspection APIs.

Stable line IDs for voice-over

One tag is special: #line:some_id. It marks a line with a stable id for localisation and voice-over lookup.

Aria: Evening, friend. #line:aria_greet_evening

That id flows onto every related event:

DialogueEvent::Line { line_id, text, .. } => {
    // line_id = Some("aria_greet_evening")
    if let Some(id) = &line_id {
        audio.play_voice_over(id);
    }
}

The id is also the key Bubbles uses when looking up translations through a LineProvider. One tag, two jobs.

Tip: Pick a naming convention early. Something like scene_speaker_variant (tavern_aria_greet_first) scales well and makes VO manifests easy to audit. The ids are never sent anywhere until you ask for them - they cost nothing at runtime.

Option groups for UI constraints

Options can be grouped with #group:<name> to signal mutual exclusivity or radio-button semantics:

Choose your faction:
-> Join the Reds #group:faction
-> Join the Blues #group:faction
-> Stay neutral

Each group value flows onto DialogueOption::group:

DialogueEvent::Options(opts) => {
    for opt in opts {
        if let Some(g) = &opt.group {
            // This option belongs to a group; you could disable others in the same group
            // e.g. if g == "faction" { /* radio-button logic */ }
        }
    }
}

The group tag itself also remains in DialogueOption::tags if you need it for styling.

Getting structured metadata from tags

Two helper functions extract special tags:

use bubbles::{line_id_from_tags, option_group_from_tags};

let id = line_id_from_tags(&tags);          // Option<String>
let group = option_group_from_tags(&tags);  // Option<String>

Both return the first match and reject empty values.

Reserved tags

Only two:

  • line:<id> - stable id for voice-over and localisation.
  • group:<name> - option group name for UI constraints.

Every other tag is yours. Bubbles won’t grow a reserved list that clashes with #combat or #boss. Go ahead and use whatever makes sense for your engine.


Try it: examples/snippets/commands.bub: see line tags (#dramatic, #sfx, #eerie) alongside commands in the transcript pane.

cargo run -p bubbles-tui -- examples/snippets/commands.bub

Next: Variables