Rez

Complex Games, Simple Authoring

For when your story wants to be a game.

The Problem with Choice-Based IF Tools

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.

How Rez Compares

Core Capabilities

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

Authoring Experience

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

Output & Distribution

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

What Rez Gives You

First-Class Game Objects

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
}

Inventories That Work

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
}

Behavior Trees for NPCs

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."]
    ]
  ]
}

Scene-Based Structure

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]
}

Procedural Generation

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;
  }
}

Dice & Probability

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

Cross-Cutting Systems

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;
  }
}

Reusable Components

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}" />

Bulma & Alpine Built In

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>

Compile-Time Validation

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.

A Taste of Rez

@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 Is For

  • Authors building mechanically complex interactive fiction
  • Developers who want structure without building from scratch
  • Twine veterans hitting walls with complex state and NPC behavior
  • Game designers who think in objects and systems

Rez Is Not For

  • Complete beginners who'd benefit from Twine's visual editor
  • Authors writing purely linear narratives
  • Developers already comfortable in Unity/Unreal
  • Anyone wanting Choice of Games' publishing platform

The Honest Trade-Off

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.

Get Started

# 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