Complex Games, Simple Authoring
For when your story wants to be a game.
You start with Twine. It's perfect for branching stories. Then you want an inventory. Then NPCs who remember things. Then relationships that affect dialogue. Suddenly you're wrestling with scattered variables, bits of Javascript, copy-pasting passage logic, and trying to make sense of a mess of passages.
Rez fills the gap: structured game objects, built-in mechanics, and a declarative language—. A game engine for Interactive Fiction.
| Capability | Twine | ChoiceScript | Ink | Rez |
|---|---|---|---|---|
| Branching narratives | Yes | Yes | Yes | Yes |
| Conditional text | Yes | Yes | Yes | Yes |
| Variables & stats | Yes | Yes | Yes | Yes |
| First-class game objects | No | No | No | Yes |
| Built-in inventory system | No | No | No | Yes |
| Relationship tracking | Manual | Manual | Manual | Built-in |
| Behavior trees / NPC AI | No | No | No | Yes |
| Procedural generation | No | No | No | Yes |
| Dice / probability systems | No | No | No | Yes |
| Scene-based structure | No | Linear | Yes (Knots and Tunnels) | Yes (Shifts and Interludes) |
| Compile-time validation | No | Basic | Yes | Yes |
| Feature | Twine | ChoiceScript | Ink | Rez |
|---|---|---|---|---|
| Visual editor | Yes | No | Inky app | No |
| Learning curve | Easy | Easy | Medium | Medium |
| Language style | Markup + JS | Indentation | Markup | Declarative |
| Custom code support | JavaScript | Custom DSL | Custom DSL | JavaScript |
| Multiple story formats | 4 formats | 1 format | 1 format | Customizable |
| Feature | Twine | ChoiceScript | Ink | Rez |
|---|---|---|---|---|
| Standalone HTML output | Yes | Yes | Via export | Yes |
| Game engine integration | No | No | Unity/Unreal | No |
| Publishing platform | Self-host | Choice of Games | Self-host | Self-host |
| Full HTML/CSS control | Yes | Limited | Via wrapper | Yes |
| Single file output | Yes | No | JSON | Yes |
Not variables scattered across passages. Structured objects with types, relationships, and behavior.
@actor sam_spade {
profession: "Private Eye"
gunplay: 6
sluething: 9
suave: 7
inventory_id: #sams_stuff
}
@rel #sam_spade -> #miss_wonderly {
affinity: 2
trust: -1
}
Slot-based inventories with type filtering. A sword fits in weapon slots. Potions have limited uses.
@inventory sams_stuff {
slots: #{#weapon_slot #pocket_slot}
}
@slot weapon_slot {
accepts: :weapon
}
@derive :weapon :item
@item revolver {
type: :weapon
damage: ^r:2d6+2
}
NPCs that make decisions, not just respond to variables.
@behaviour barfly_behavior {
tree: ^[$selector
[$sequence
[check_state state=:thirsty]
[check_location type=:bar]
[say msg="I need a drink."]
]
[$sequence
[check_state state=:bored]
[say msg="Nothing happens here."]
]
]
}
Dialogue scenes work differently than combat. Cards can stack (conversations) or replace (locations).
@scene interrogation_scene {
initial_card_id: #enter_interrogation
on_start: (scene) => {
$game.setMood("tense");
}
}
@scene inventory_scene {
blocks: [panel: #inventory_panel]
}
Spawn enemies, generate loot, create variations—all declarative.
// Template actors get copied at runtime
@actor thug {
$template: true
name: "Thug"
health_roll: ^r:2d+2
health: _
toughness_roll: ^r:3d6
toughness: _
}
// Groups filter assets by type and tags
@group valuable_loot {
type: :asset
include_tags: #{:valuable :portable}
exclude_tags: #{:quest_item}
}
// Create copies in event handlers
const enemy = $thug.addCopy()
// Or use generators to make multiple copies
@generator gang {
source_id: #thug
copies: ^2d10+10
on_copy: (thug) => {
thug.name = $("thug_names").next_name();
thug.health = thug.health_roll;
thug.toughness = thug.toughness_roll;
}
}
RPG mechanics without building a dice system.
@item revolver {
damage: ^r:2d6+2
}
// auto-rolls on access
const damage = $revolver.damage;
// Lists for random selection
@list thug_names {
$global: true
next_name: function() {
// no repeats until all used
return this.randomWalk();
}
values: ["Knuckles" "Mugsy" "Vince"]
}
// Probability tables for weighted random
@actor thug {
$template: true
name: ^i{$thug_names.next_name()}
encounter:
|#common_thug 60
#armed_guard 25
#crime_boss 15|
}
$("thug").encounter;
// 60% chance of a common thug
// 25% chance of an armed guard
// 15% chance of a crime boss
Weather, time, wandering NPCs—mechanics that affect everything.
@system time_system {
priority: 10
turns: 0
after_event: (clock, evt, result) => {
if(evt.target.dataset.end_turn) {
$game.move_actors();
clock.turns += 1;
}
return result;
}
}
Build your own template components for consistent UI. Use them anywhere with a simple syntax.
@component health_bar (bindings, assigns, content) => {
const current = assigns.current;
const max = assigns.max;
const pct = (current / max) * 100;
return `
<div class="health-bar">
<div class="fill" style="width:${pct}%"></div>
<span>${current}/${max}</span>
</div>
`;
}
// Use it anywhere
<.health_bar current="${player.health}" max="${player.max_health}" />
Rez ships with Bulma CSS and Alpine.js for polished UI and reactivity out of the box. Prefer Bootstrap or Tailwind? Swap them in.
// Bulma classes work immediately
<div class="box">
<div class="columns">
<div class="column is-half">...</div>
</div>
</div>
// Alpine.js for reactive UI
<div x-data="{open: false}">
<button @click="open = !open">Toggle</button>
<div x-show="open">Hidden content</div>
</div>
Catch typos before playtest. Schema validation ensures your game structure is correct.
// Rez catches errors like:
// - References to non-existent objects
// - Missing required attributes
// - Type mismatches
// - Invalid relationships
// Before you ever run the game.
@game {
name: "The Maltese McGuffin"
initial_scene_id: #office_scene
}
@scene office_scene {
initial_card_id: #c_office_intro
}
@card c_office_intro {
bindings: {wonderly: #miss_wonderly, player: #player}
content: ```
<p>Rain drums against the window. The phone hasn't rung in three days.</p>
<p>Then she walks in. The kind of dame who makes you forget you're broke.</p>
$if(wonderly.trust > 0) -> {%
<p>She seems nervous, but genuine.</p>
%} () -> {%
<p>Something about her story doesn't add up.</p>
%}
<ul>
<li><a card="c_ask_sister">Ask about her "missing sister"</a></li>
<li><a card="c_demand_truth">Demand to know who sent her</a></li>
$if(player.hasItem("whisky")) -> {%
<li><a card="c_share_drink">Share a drink</a></li>
%}
</ul>
```
}
Conditional text. Conditional choices. Object references. All readable.
Rez has a steeper learning curve than Twine. There's no visual editor. You write source files and run a compiler.
But the curve pays off when your game has:
Rez gives you the complexity your game needs, without the complexity it doesn't.
# Install Rez
# Download & extract the pre-built binary for your OS:
# https://github.com/mmower/rez/releases/latest
# Make it available in your path
# Create a new project (e.g. on macOS)
rez_macos new my_game
# Compile and play
cd my_game
rez_macos compile src/my_game.rez
open dist/index.html
Output: A single HTML file. No runtime dependencies. Host anywhere.
View on GitHub