urd 0.1.0

Dialogue scripting language for games — compiler, VM, and static analysis
Documentation
# Urd

Urd is a dialogue scripting language designed to be easily embeddable into any game engine.

It compiles `.urd` scripts into a directed graph that a lightweight VM walks one step at a time,
yielding events your engine decides how to render. Urd doesn't care about your UI, your renderer,
or your opinions — it just emits dialogue and choice events and gets out of the way.

## Quick taste

```urd
const narrator = :{ name: "Narrator", name_color: "white" }
const zara     = :{ name: "Zara",     name_color: "cyan"  }

global gold      = 0
global has_torch = false

@entry
label start {
    narrator: {
        "The wind howls across the barren moor."
        "Before you yawns the entrance to an ancient cave."
    }

    zara: "Halt, traveler. That cave has swallowed many who entered unprepared."

    menu {
        "Enter the cave" {
            jump cave_entrance
        }
        "Take a torch from Zara" {
            has_torch = true
            zara: "Here — take this torch. May it keep the shadows at bay."
            jump cave_entrance
        }
        "Walk away" {
            narrator: "The Forgotten Cave remains just that — forgotten."
            end!()
        }
    }
}
```

## Notable features

### Dialogue is a first-class construct

Speakers are values, not magic strings. Define a struct, give your characters names and colors
(or whatever metadata your engine needs), and write dialogue that reads like a screenplay:

```urd
struct Character {
    name: str
    name_color: str
}

const merchant: Character = :{ name: "Aldric", name_color: "#42c8f5" }

merchant: "Welcome, stranger! These are dark times."
```

Multi-line dialogue blocks keep things tidy:

```urd
narrator: {
    "In the age before the Sundering, three great powers held the world"
    "in uneasy balance: the Guild, the Empire, and the Rebels."
}
```

### Branching with menus

Player choices are expressed with `menu` blocks. Each option has a label and a body:

```urd
menu {
    "Ask about the Guild" {
        jump guild_lore
    }
    "Buy supplies and head out" {
        gold = gold - 10
        jump dungeon_entry
    }
    "Sleep on it" {
        narrator: "You wake refreshed. The crossroads awaits."
        jump choose_path
    }
}
```

### Labels and jumps

Labels are named sections of dialogue. `jump` transfers control. Think `goto`, but for conversations:

```urd
@entry
label start {
    narrator: "Welcome."
    jump chapter_one
}

label chapter_one {
    narrator: "It begins."
    end!()
}
```

Need subroutine-style calls? `jump ... and return` pushes a call frame:

```urd
let result = jump some_helper and return
```

### Conditionals and match

Standard `if`/`elif`/`else`:

```urd
if gold >= 60 {
    narrator: "The bards will sing of this."
} elif gold >= 10 {
    narrator: "Not empty-handed, and still breathing."
} else {
    narrator: "Survival is its own reward."
}
```

Pattern matching on enums, literals, ranges, and dice:

```urd
enum Faction { Guild, Empire, Rebel }

match player_faction {
    Faction.Guild  { hero: "I used to be one of them." }
    Faction.Empire { hero: "The Empire never trusted the Guild." }
    Faction.Rebel  { hero: "Nobody listened." }
}
```

### String interpolation

```urd
narrator: "You carry {gold} gold and {inventory_count} item(s)."
narrator: "Your {starting_dagger.name} is still sharp enough. Barely."
```

### Dice as first-class values

Roll dice, match on results:

```urd
let roll = 2d6

match roll {
    2..=4  { narrator: "Critical failure." }
    5..=9  { narrator: "Partial success." }
    10..=12 { narrator: "Critical hit!" }
}
```

### Multi-file imports

Split your story across files and import what you need:

```urd
import "village.urd" as village
import (Faction, narrator, hero) from "characters.urd"
import Item from "items.urd"
```

Circular imports work fine — the compiler handles it.

### Decorators

Attach custom metadata to dialogue events. Define them in-script or register them from your engine at runtime:

```urd
decorator slow<event: dialogue>(speed: float) {
    event.text_speed = speed
}

@slow(0.3)
narrator: "Time seemed to stop..."
```

The built-in `@entry` marks where execution begins. `@fluent` plugs variables into
[Project Fluent](https://projectfluent.org/) for localization.

### Localization

First-class Fluent integration. Tag globals with `@fluent`, generate `.ftl` scaffolds,
and let translators handle pluralization and grammatical case without touching your scripts:

```urd
@fluent
global gold = 50

narrator: "You have {gold} gold."
```

### Functions

Pure functions for when you need logic but not dialogue:

```urd
fn clamp(value: int, low: int, high: int) -> int {
    if value < low { return low }
    if value > high { return high }
    return value
}
```

### Frontend-agnostic VM

The VM is pull-based. Your engine drives the loop:

```rust
let mut vm = Vm::new(graph, registry)?;

loop {
    match vm.next(None) {
        VmStep::Event(Event::Dialogue { speakers, lines, .. }) => {
            // render however you want
        }
        VmStep::Event(Event::Choice { options, .. }) => {
            let picked = show_menu(&options);
            vm.next(Some(picked));
        }
        VmStep::Ended => break,
        VmStep::Error(e) => eprintln!("runtime error: {e}"),
    }
}
```

Urd doesn't draw anything. It doesn't know what a screen is. It yields structured events
and your engine does whatever it wants with them.

## Tooling

- **Tree-sitter grammar** — syntax highlighting and indentation for Neovim, Helix, and Zed
- **Language server** (`urd-lsp`) — diagnostics, hover, go-to-definition, rename, completion, and spellcheck
- **CLI runner** (`quest`) — run scripts in the terminal, export graphs, generate localization scaffolds
- **20+ static analysis passes** — from type checking and exhaustiveness to typo detection and infinite loop warnings

## License

MIT