Language Guide
Matt Mower
- Part 1: Getting Started
- Part 2: Core Concepts
- Part 3: Building Interactive Content
- Part 4: Advanced Features
- Part 5: Technical Reference
Part 1: Getting Started
What is Rez?
Rez is a language for creating choice-based HTML games/interactive fiction.
If you have ever wanted to build choice-based games with inventories, smart NPCs, and complex mechanics without fighting your tools, Rez may be for you.
Think of it as a powerful alternative to Twine. While Twine democratized interactive fiction creation and remains excellent for narrative-focused projects, more complex games push the limits and need a tool designed to support that complexity. This is where Rez comes into its own.
Rez uses an easy-to-learn declarative syntax and only requires Javascript for advanced features.
It makes it easy to build:
-
Rich NPCs with memory, behavior trees, and complex interactions
-
Dynamic inventories where a ring works differently on your hand vs in your backpack
-
Flexible layouts that adapt to different game requirements (combat, dialogue, exploration)
-
Procedural content that creates unique experiences on each playthrough
-
New mechanics that we didn’t think of
Rez sits between the simplicity of Twine and the complexity of parser-based systems like Inform while offering many of the benefits of both.
A Simple Example
Here’s a taste of what Rez code looks like:
@card greeting { visit_count: 0 content: ``` <p>Hello! This is visit number ${card.visit_count}.</p> <p><a card="greeting">Visit again</a></p> ``` on_start: (card) => { card.visit_count += 1; } }
This creates an interactive card that tracks how many times it’s been visited.
This tiny example demonstrates Rez’s core philosophy: clean separation between what the players see and what happens behind the scenes. Scale this approach up and you can build any choice-based game you can imagine.
Installation and Setup
To get started with Rez, you’ll need to have Rez installed on your system.
Note
|
Installation instructions depend on your platform. Please refer to the main Rez documentation for current installation methods. |
Once installed, you can create a new game project with:
rez new my_first_game --author-name="Your Name" --author-email="you@example.com" --game-title="My First Game"
This creates a folder called my_first_game
with the following structure:
my-first-game/ ├── src/ │ └── my-first-game.rez # Your game source code ├── lib/ │ └── stdlib.rez # Rez standard library (don't modify) ├── assets/ # Images, sounds, etc. └── dist/ # Compiled game files (created on build)
Your First Game
Let’s create a simple but complete game to understand the basics. Open src/my-first-game.rez
and replace everything from @game
onwards with:
@game { name: "treasure_hunt" title: "Treasure Hunt" author_name: "Matt Mower" author_email: "self@mattmower.com" game_homepage: "http://mattmower.com/" IFID: "12345678-1234-5678-9ABC-123456789ABC" archive_format: 1 version: 1.0 created: "2025-07-20 14:01:27.161254Z" initial_scene_id: #opening_scene layout: ``` <section class="main-content is-fullheight"> <section class="hero"> <div class="hero-body"> <p class="title">${game.title}</p> <p class="sub_title">a game by <a href="${game.game_homepage}">${game.author_name}</a></p> </div> </section> <div class="container"> ${content} </div> </section> ``` } @scene opening_scene { initial_card_id: #forest_entrance } @card forest_entrance { content: ``` <p>You stand at the edge of a dark forest. A weathered sign reads "TREASURE WITHIN".</p> <p>What do you want to do?</p> <ul> <li><a card="enter_forest">Enter the forest</a></li> <li><a card="examine_sign">Examine the sign more closely</a></li> </ul> ``` } @card enter_forest { content: ``` <p>You push through the undergrowth and find yourself in a small clearing. In the center sits an old wooden chest!</p> <p><a card="open_chest">Open the chest</a></p> <p><a card="forest_entrance">Go back to the entrance</a></p> ``` } @card examine_sign { content: ``` <p>Looking closer at the sign, you notice small text at the bottom: "Beware the guardian of the treasure."</p> <p><a card="forest_entrance">Return to the entrance</a></p> ``` } @card open_chest { bindings: [player: #player] content: ``` $if(player.has_key) -> {% <p>Using the key, you open the chest and find a pile of gold coins! You win!</p> <p><a card="forest_entrance">Play again</a></p> %} () -> {% <p>The chest is locked tight. You need a key.</p> <p><a card="search_area">Search the area for a key</a></p> <p><a card="enter_forest">Go back</a></p> %} ``` } @card search_area { content: ``` <p>You search around the clearing and find a rusty key hidden under some leaves!</p> <p><a card="open_chest">Try the key on the chest</a></p> ``` on_start: (card) => { $player.has_key = true; } } @actor player { $global: true has_key: false }
Note
|
Notice the |
Compiling and Running the Game
From the my_first_game
folder run:
➜ rc compile src/my_first_game.rez rez v1.8.1 — compiled to dist folder
Then open the dist/index.html file. There’s a version online and it should look like:

Distributing Your Game
Package the contents of the dist
folder in any way you like.
Understanding the Structure
Let’s break down what we just created:
@game: The top-level container that defines the game’s metadata and overall layout. Every Rez game needs exactly one @game`
element and it always has the id #game
. It must also have an attribute initial_scene_id
that refers to the scene that starts the game.
@scene: A context where interactions take place, can have a layout (for example a dialogue scene might use a different layout to an inventory scene). Each scene has an initial_card_id
that defines which @card`
gets presented first when that scene starts. You can have one scene or many scenes.
@card: The content that players mostly see and interact with. Cards contain the text, choices, and behaviors that make up your game. It must have a content:
attribute containing a template that determines what the player will be shown.
The structure flows like this:
@game layout └── @scene layout └── @card content
Compilation and Testing
To build your game, run:
rez compile src/my-first-game.rez
This creates the compiled game in the dist/
folder. Open dist/index.html
in a web browser to play your game.
The compilation process:
-
Reads your
.rez
source files -
Converts them to JavaScript objects
-
Creates an HTML page with embedded JavaScript
-
Copies any assets to the distribution folder
Next Steps
Now that you have a working game, you can:
-
Add more cards and scenes to expand the story
-
Include images and other assets
-
Add dynamic behavior with JavaScript
-
Experiment with different layout modes
In the next section, we’ll dive deeper into the core concepts that make Rez games work.
Part 2: Core Concepts
Understanding Elements
Rez games are built from elements - the building blocks that define everything in your game. Elements represent things like the game itself, scenes, cards, characters, items, and more.
Every element follows the same basic pattern:
@element_type unique_id { attribute_name: value another_attribute: another_value }
For example:
@card magic_sword { name: "Enchanted Blade" damage: 15 magic: true }
The Three Essential Elements
Every Rez game uses three core elements:
@game - The overall game container that holds game metadata and the "master" layout
@game { name: "adventure" title: "Sandbags Mythical Adventure" initial_scene_id: #opening author: "Matt Mower" author_email: "self@mattmower.com" layout: ``` <section class="hero"> <div class="hero-body"> <p class="title">${game.title}</p> <p class="subtitle">By ${game.author}</p> </div> </section> ${content} ``` }
@scene - A context for a kind of player interaction that can have its own layout
@scene opening { initial_card_id: #start_here layout: ``` <div class="columns-2"> <div class="column">${sidebar}</div> <div class="column">${content}</div> </div> ``` }
@card - Content that players see and interact with
@card start_here { content: ``` <p>Your adventure begins...</p> <p><a card="next_card">Continue</a></p> ``` }
Element IDs
Every element needs a unique ID that follows JavaScript naming rules:
-
Start with a letter, underscore, or dollar sign
-
Can contain letters, numbers, underscores, or dollar signs
-
Cannot contain spaces or special characters
-
Are case-sensitive
Good IDs: forest_entrance
, magic_sword
, npc_wizard
, scene1
Bad IDs: forest entrance
, magic-sword
, 123start
, npc@wizard
Understanding Attributes
Attributes are how you describe an element’s properties and behavior. They use the format name: value
with a required space after the colon.
@card treasure_room { title: "The Treasure Chamber" // String visited: false // Boolean gold_count: 100 // Number room_type: :treasure // Keyword (symbol) owner_id: #dragon // Element reference content: ``` // Template (multi-line string) <p>A room filled with gold!</p> ``` on_enter: (card) => { // Event script function card.visited = true; } refill_gold: function() { // Action script function this.gold_count = 100; } }
Attribute Types
Strings - Text enclosed in double quotes
description: "A rusty old key"
Numbers - Integers or decimals
damage: 15 weight: 2.5 price: -10
Booleans - True or false values
magic: true cursed: false found: yes // yes/no also work hidden: no
Keywords - Symbols starting with a colon, useful for categories
material: :gold rarity: :legendary type: :weapon
Element References - Point to another elements by referencing it’s id #
owner_id: #player location_id: #forest wielded_by_id: #hero
Important
|
The @scene forest { initial_card_id: #entrance // You define this // Rez automatically creates: // initial_card -> the actual RezCard object } @actor hero { location_id: #forest // You define this // Rez automatically creates: // location -> reference to the object with ID #forest } This means you can use both |
Templates - Multi-line content with dynamic expressions
content: ``` <p>Welcome, ${player.name}!</p> <p>You have ${player.gold} gold coins.</p> ```
Lists - Collections of values (space-separated, no commas)
colors: [:red :green :blue] numbers: [1 2 3 4 5] names: ["Alice" "Bob" "Charlie"] items: [#sword #shield #potion]
Event Scripts - JavaScript functions for handling events
on_use: (item) => { player.health += 10; return RezEvent.playCard("feeling_better"); }
Action Script - Javascript functions for performing behaviours (this
is automatically the element on which the function is defined).
use: function() { this.uses -= 1; }
Tip
|
Event Response Methods: Event scripts must use RezEvent to specify what happens as a result of the event:
Methods can be chained: |
Dynamic Properties
Dynamic properties are attributes that calculate their value each time they’re accessed, rather than storing a fixed value. They’re perfect for derived stats, percentages, and values that depend on other attributes.
Basic Dynamic Properties
Use ^p{…}
to define a property that recalculates automatically:
@actor hero { first_name: "Sir" last_name: "Galahad" // This calculates every time it's accessed full_name: ^p{ return `${this.first_name} ${this.last_name}`; } health: 85 max_health: 100 // Perfect for UI elements like health bars health_percentage: ^p{ return this.health * 100 / this.max_health; } // Boolean states based on other attributes is_healthy: ^p{ return this.health > this.max_health * 0.5; } is_at_full_health: ^p{ return this.health >= this.max_health; } }
Practical Examples
Dynamic properties shine for calculated stats:
@actor character { strength: 15 dexterity: 12 intelligence: 14 // Calculated modifier (D&D style) strength_modifier: ^p{ return Math.floor((this.strength - 10) / 2); } // Total of all stats total_stats: ^p{ return this.strength + this.dexterity + this.intelligence; } // Armor class calculation base_armor: 10 armor_bonus: 2 armor_class: ^p{ return this.base_armor + this.dex_modifier + this.armor_bonus; } }
For inventory and resource management:
@actor player { gold: 150 silver: 75 copper: 200 // Total wealth in copper pieces total_wealth: ^p{ return this.copper + (this.silver * 10) + (this.gold * 100); } current_weight: 45 max_carrying_capacity: 100 // How much more can be carried carrying_capacity_remaining: ^p{ return Math.max(0, this.max_carrying_capacity - this.current_weight); } // Whether player is overloaded is_encumbered: ^p{ return this.current_weight > this.max_carrying_capacity; } }
When Properties Recalculate
Dynamic properties recalculate every time they’re accessed. This means:
-
When displayed in templates:
${player.health_percentage}
-
When used in conditions:
$if(player.is_healthy)
-
When accessed in JavaScript:
player.total_stats
The calculation runs fresh each time, so changes to underlying attributes automatically update derived values.
Tip
|
Dynamic properties are ideal for:
Avoid using them for expensive calculations that don’t change often - use regular attributes for those. |
Basic Content and Templates
The content
attribute is where you define what players actually see. It uses templates that can include both static text and dynamic expressions.
Static Content
Simple text and HTML:
@card welcome { content: ``` <h2>Welcome to the Adventure!</h2> <p>You are standing in a medieval village square.</p> ``` }
Dynamic Content with Expressions
Use ${…}
to include dynamic values:
@card status { content: ``` <h3>Character Status</h3> <p>Name: ${player.name}</p> <p>Level: ${player.level}</p> <p>Health: ${player.health}/${player.max_health}</p> ``` }
Note that expressions cannot be arbitrary pieces of Javascript so ${player.level + 1}
while it will compile, is not a valid expression.
The player
reference comes from bindings - you tell Rez which game objects to make available in templates:
@card status { bindings: [player: #main_character] content: ``` <p>Name: ${player.name}</p> ``` }
Bindings flow down so that any binding made in the @game
is available in the current @scene
or @card
, likewise bindings in the @scene
are available in the current card.
Default Bindings
Some bindings are always available:
-
card
- The current card being displayed -
scene
- The current scene -
game
- The game object
@card example { visit_count: 0 content: ``` <p>This card has been visited ${card.visit_count} times.</p> ``` }
Navigation Between Cards
Rez provides several ways to move between cards and scenes:
Playing a Card
Use <a card="card_id">
to switch to another card in the same scene:
<a card="forest_clearing">Enter the forest</a>
Switching Scenes
Use <a scene="scene_id">
to start a new scene:
<a scene="dungeon_entrance">Enter the dungeon</a>
Scene Interludes
Use <a interlude="scene_id>
and <a resume>
to run a scene interlude.
Sometimes mid-scene we find a need to go to another scene temporarily. This is very common for things like NPC conversation or combat, shops/trading or inventory management, presenting history or other details, or for mini-games.
Using an interlude we 'put a bookmark' in the current scene, switch another scene, and then use the bookmark to find our place in the original scene and continue where we left off.
<a interlude="conversation">Talk to the Hermit</a>
When the user clicks an <a resume>
link the interlude scene goes away and the original scene is resumed from the point where the player left it.
Event Links
Trigger custom behavior with <a event="event_name">
:
@card magic_door { locked: true content: ``` <p>A mysterious door blocks your path.</p> $if(card.locked) {% <a event="try_key">Try to unlock with the key</a> %}, {% <a card="beyond_door">Go through the door</a> %} ``` on_try_key: (card) => { if(player.has_item("magic_key")) { card.locked = false; return RezEvent.flash("The key works! The door swings open."); } else { return RezEvent.flash("You don't have the right key."); } } }
Tip
|
A big part of keeping your game managable is making good decisions about where to store the different properties that describe the 'world model'. Sometimes it is okay to put it in the nearest card but in different situations it might be more appropriate to put it elsewhere. Obvious places are the player (if you have one) or the game. However both of these can end up "stuffed" with properties relating to the world or puzzles you are setting up for the player. Sometimes you can ameliorate this with comments but often the answer is to find a better place. For example you might think about using a @object magic_door { locked: true description: "mysterious door" } If there were multiple ways that this door could be unlocked then having it contained in its own object could make a lot of sense and be much easier to understand later on. |
Buttons
You can also use buttons instead of links:
<button card="next_scene">Continue Adventure</button> <button event="cast_spell">Cast Fireball</button>
Custom Components
Rez allows you to create reusable HTML components using @component
. Components are JavaScript functions that return formatted HTML and can accept parameters, making them perfect for complex UI elements that you use repeatedly.
Basic Component Syntax
@component component_name (bindings, assigns, content) => { // JavaScript code that returns HTML string return `<div>Custom HTML here</div>`; }
Simple Self-Closing Component
@component health_bar (bindings, assigns, content) => { const current = assigns.current || 0; const max = assigns.max || 100; const percentage = Math.round((current / max) * 100); return ` <div class="health-bar"> <div class="health-fill" style="width: ${percentage}%"></div> <span class="health-text">${current}/${max}</span> </div> `; }
Use it in templates with dot notation as a self-closing tag:
content: ``` <.health_bar current="${player.health}" max="${player.max_health}" /> ```
Container Component with Content
Components can also wrap content like regular HTML tags:
@component info_panel (bindings, assigns, content) => { const title = assigns.title || "Information"; const type = assigns.type || "default"; return ` <div class="info-panel info-panel--${type}"> <h3 class="info-panel__title">${title}</h3> <div class="info-panel__content"> ${content} </div> </div> `; }
Use it as a container with dot notation:
content: ``` <.info_panel title="Combat Status" type="warning"> <p>You are in combat with ${enemy.name}!</p> <p>Your health: ${player.health}</p> </.info_panel> ```
Advanced Component with Logic
Components can include complex logic and conditionals:
@component inventory_slot (bindings, assigns, content) => { const item = assigns.item; const index = assigns.index; const isEmpty = !item; if (isEmpty) { return ` <div class="inventory-slot inventory-slot--empty" data-slot="${index}"> <div class="slot-placeholder">Empty</div> </div> `; } const rarity = item.rarity || "common"; const quantity = item.quantity > 1 ? `<span class="quantity">${item.quantity}</span>` : ""; return ` <div class="inventory-slot inventory-slot--${rarity}" data-slot="${index}"> <img src="${item.icon}" alt="${item.name}" class="item-icon"> <div class="item-name">${item.name}</div> ${quantity} <div class="item-tooltip" data-tooltip="${item.description}"></div> </div> `; }
Component Parameters
Components receive three parameters:
-
bindings
- All bindings available in the current context (player, scene, etc.) -
assigns
- Attributes passed to the component via HTML attributes -
content
- HTML content between opening and closing tags (for container components)
When to Use Components
Components are ideal for:
-
Repeated UI patterns - health bars, inventory slots, dialog boxes
-
Complex widgets - skill trees, character sheets, mini-maps
-
Conditional rendering - different layouts based on game state
-
Dynamic content - procedurally generated elements
-
Reusable game mechanics - card displays, battle interfaces
Tip
|
Keep components focused on presentation logic. For game logic, use element methods and events instead. Components should transform data into HTML, not modify game state. |
A Complete Example
Let’s build a simple character creation scene:
@scene character_creation { initial_card_id: #choose_name layout: ``` <div class="creation-scene"> <h2>Create Your Character</h2> ${content} </div> ``` } @card choose_name { content: ``` <p>What is your character's name?</p> <input type="text" rez-bind="player.name" placeholder="Enter name"> <p><button card="choose_class">Next: Choose Class</button></p> ``` } @card choose_class { bindings: [player: #player] content: ``` <p>Hello, ${player.name}! Choose your class:</p> <ul> <li><a event="choose_warrior">Warrior (Strong and tough)</a></li> <li><a event="choose_mage">Mage (Wise and magical)</a></li> <li><a event="choose_thief">Thief (Quick and sneaky)</a></li> </ul> ``` on_choose_warrior: (card) => { player.class = "warrior"; player.strength = 15; player.sneak = 5; player.magic = 5; return RezEvent.playCard("creation_complete"); } on_choose_mage: (card) => { player.class = "mage"; player.strength = 5; player.sneak = 5; player.magic = 15; return RezEvent.playCard("creation_complete"); } on_choose_thief: (card) => { player.class = "thief"; player.strength = 5; player.sneak = 15; player.magic = 5; return RezEvent.playCard("creation_complete"); } } @card creation_complete { bindings: [player: #player] content: ``` <h3>Character Created!</h3> <p><strong>Name:</strong> ${player.name}</p> <p><strong>Class:</strong> ${player.class}</p> <p><strong>Strength:</strong> ${player.strength}</p> <p><strong>Magic:</strong> ${player.magic}</p> <p><button scene="game_start">Begin Adventure!</button></p> ``` } @actor player { $global: true name: "" class: "" strength: 5 magic: 5 sneak: 5 }
This example demonstrates:
-
Multiple cards working together
-
Form input with
rez-bind
-
Event handlers that modify game state
-
Dynamic content based on character attributes
-
Navigation between cards and scenes
Part 3: Building Interactive Content
Advanced Templating
Rez templates support more than just simple variable substitution. You can create dynamic content that responds to game state, user choices, and changing conditions.
Conditional Content
Use $if()
expressions to show content based on conditions:
@card tavern { bindings: [player: #hero] content: ``` <h3>The Rusty Dragon Tavern</h3> $if(player.gold >= 10) -> {% <p>You have enough gold for a meal and room.</p> <button event="buy_room">Rent a room (10 gold)</button> %} () -> {% <p>You're too poor to afford a room here.</p> <button event="work_for_gold">Offer to work for gold</button> %} $if(player.level >= 5) -> {% <p>The bartender nods respectfully at such an experienced adventurer.</p> %} ``` }
You can chain conditions:
$if(player.health <= 0) -> {% <p>You have died!</p> %} (player.health < 20) -> {% <p>You are badly wounded.</p> %} () -> {% <p>You feel healthy and strong.</p> %}
Iterating Over Collections
Use $foreach()
to loop through lists or arrays:
@card inventory { bindings: [ player: #hero, items: () => $("hero").inventory.items ] content: ``` <h3>Your Inventory</h3> $if(items.length > 0) {% <ul> $foreach(item: items) {% <li>${item.name} - ${item.description}</li> %} </ul> %}, {% <p>Your inventory is empty.</p> %} ``` }
With separators between items:
$foreach(skill: player.skills) {% ${skill.name}: Level ${skill.level} %}, {% <br> %}
Advanced Bindings
Bindings make data available to your templates. You can create different types of bindings:
Element Bindings - Reference game objects:
bindings: [ player: #main_character, location: #current_room, villain: #dark_lord ]
Function Bindings - Calculate values dynamically:
bindings: [ total_score: () => {return player.experience + player.gold}, random_event: () => {return Math.random() < 0.3}, available_exits: () => {return current_location.getExits()} ]
Attribute Bindings - Direct access to specific attributes:
bindings: [ player_name: &player.name, current_health: &player.health ]
Forms and User Input
Rez makes it easy to capture user input through HTML forms with two-way data binding.
Text Input
Bind text inputs directly to object attributes:
@card character_setup { bindings: [player: #player] content: ``` <h3>Character Setup</h3> <label>Character Name:</label> <input type="text" rez-bind="player.name" placeholder="Enter your name"> <label>Background Story:</label> <textarea rez-bind="player.background" rows="4"></textarea> <button card="choose_stats">Continue</button> ``` }
Checkboxes and Choices
Use checkboxes for boolean values:
@card preferences { bindings: [settings: #game_settings] content: ``` <h3>Game Preferences</h3> <label> <input type="checkbox" rez-bind="settings.sound_enabled"> Enable Sound Effects </label> <label> <input type="checkbox" rez-bind="settings.difficulty_hard"> Hard Difficulty Mode </label> ``` }
Radio buttons for exclusive choices:
<h4>Choose your weapon:</h4> <label> <input type="radio" name="weapon" value="sword" rez-bind="player.starting_weapon"> Sword (+3 Attack) </label> <label> <input type="radio" name="weapon" value="bow" rez-bind="player.starting_weapon"> Bow (+2 Attack, +1 Range) </label> <label> <input type="radio" name="weapon" value="staff" rez-bind="player.starting_weapon"> Staff (+1 Attack, +2 Magic) </label>
Select Dropdowns
For choosing from many options:
<label>Starting Location:</label> <select rez-bind="player.hometown"> <option value="village">Peaceful Village</option> <option value="city">Bustling City</option> <option value="forest">Forest Dwelling</option> <option value="mountain">Mountain Keep</option> </select>
Event-Driven Forms
Use rez-live
to trigger events when form values change:
@card dynamic_stats { content: ``` <h3>Allocate Stat Points</h3> <p>Points remaining: ${card.points_remaining}</p> <label>Strength: <input type="range" min="1" max="20" rez-bind="player.strength" rez-live> </label> <label>Intelligence: <input type="range" min="1" max="20" rez-bind="player.intelligence" rez-live> </label> ``` points_remaining: 20 on_input: (card, event) => { const total = player.strength + player.intelligence; card.points_remaining = 20 - total; return RezEvent.render(); } }
Assets and Media
Add images, audio, and other media to your games using the asset system.
Defining Assets
Create asset elements to register your media files:
@asset hero_portrait { file: "characters/hero.png" width: 150 height: 200 } @asset background_music { file: "audio/tavern_theme.mp3" } @asset treasure_sound { file: "audio/treasure_found.wav" }
Using Images
Display images in your content:
@card character_sheet { content: ``` <h3>Character Information</h3> <div class="character-display"> <.img name="hero_portrait" class="character-image" /> <div class="character-stats"> <p><strong>Name:</strong> ${player.name}</p> <p><strong>Level:</strong> ${player.level}</p> </div> </div> ``` }
Or build your own image tags:
<img src='${"hero_portrait" | asset_path}' alt="Hero Portrait" class="portrait">
Dynamic Assets
Choose different assets based on game state:
@card location_view { bindings: [ location_image: () => { if (player.location === "forest") return "forest_scene"; if (player.location === "dungeon") return "dungeon_scene"; return "default_scene"; } ] content: ``` <div class="location-display"> <img src='${location_image | asset_path}' alt="Current Location"> <p>${location.description}</p> </div> ``` }
Audio Integration
While audio isn’t fully integrated yet, you can add it manually:
@card victory { content: ``` <h2>Victory!</h2> <p>You have defeated the dragon and claimed the treasure!</p> <audio autoplay> <source src='${"victory_music" | asset_path}' type="audio/mpeg"> </audio> ``` }
Building a Complete Interactive Scene
Let’s create a combat system that demonstrates advanced templating and dynamic content:
@scene combat { initial_card_id: #combat_start enemy_id: #goblin bindings: [ player: #player enemy: scene.enemy ] layout: ``` <div class="combat-scene"> <h2>Combat!</h2> ${content} </div> ``` } @card combat_start { content: ``` <div class="combat-status"> <div class="combatant"> <h4>${player.name}</h4> <div class="health-bar"> <div class="health-fill" style="width: ${player.health_perc}%"></div> </div> <p>Health: ${player.health}/${player.max_health}</p> </div> <div class="vs">VS</div> <div class="combatant"> <h4>${enemy.name}</h4> <div class="health-bar"> <div class="health-fill" style="width: ${enemy.health_perc}%"></div> </div> <p>Health: ${enemy.health}/${enemy.max_health}</p> </div> </div> <div class="combat-actions"> <h4>Choose your action:</h4> <button event="attack">Attack (${player.attack_power} damage)</button> <button event="defend">Defend (+${player.defense_bonus} defense)</button> $if(player.mana >= 10) {% <button event="cast_spell">Cast Fireball (10 mana)</button> %} <button event="try_flee">Attempt to Flee</button> </div> ``` on_attack: (card) => { const enemy = card.scene.enemy; const damage = Math.max(1, $player.attack_power - enemy.defense); enemy.health -= damage; if (enemy.health <= 0) { return RezEvent.playCard("victory"); } else { return RezEvent.playCard("enemy_turn").flash(`You deal ${damage} damage!`); } } on_defend: (card) => { $player.temp_defense = $player.defense_bonus; return RezEvent.playCard("enemy_turn").flash("You prepare to defend!"); } on_cast_spell: (card) => { const enemy = card.scene.enemy; $player.mana -= 10; const damage = 15; enemy.health -= damage; if(enemy.health <= 0) { return RezEvent.playCard("victory"); } else { return RezEvent.playCard("enemy_turn").flash(`Your fireball deals ${damage} damage!`); } } on_try_flee: (card) => { const flee_ability = 0.5 * (($player.sneak - 7.5) / 7.5); const flee_chance = 0.5 + flee_ability; if (Math.random() < flee_chance) { return RezEvent.switchScene("overworld").flash("You successfully escape!"); } else { $player.temp_defence = -5; return RezEvent.playCard("enemy_turn").flash("You couldn't escape!"); } } } @card enemy_turn { content: ``` <p>${enemy.name} attacks!</p> <button event="continue">Continue</button> ``` on_continue: (card) => { const enemy = card.scene.enemy; const damage = Math.max(1, enemy.attack_power - ($player.defense + ($player.temp_defense || 0))); player.health -= damage; player.temp_defense = 0; // Reset temporary defense if (player.health <= 0) { return RezEvent.playCard("defeat"); } else { return RezEvent.playCard("combat_start").flash(`${enemy.name} deals ${damage} damage to you!`); } } } @card victory { content: ``` <h3>Victory!</h3> <p>You have defeated the ${enemy.name}!</p> <p>You gain ${enemy.experience_reward} experience and ${enemy.gold_reward} gold.</p> <button scene="overworld">Continue your adventure</button> ``` on_start: (card) => { const enemy = card.scene.enemy; player.experience += enemy.experience_reward; player.gold += enemy.gold_reward; } } @actor player { $global: true name: "Hero" health: 100 max_health: 100 health_perc: ^p{ return this.health * 100 / this.max_health; } mana: 50 attack_power: 15 defense: 5 defense_bonus: 3 experience: 0 gold: 0 } @actor goblin { $global: true name: "Goblin" health: 30 max_health: 30 health_perc: ^p{ return this.health * 100 / this.max_health; } attack_power: 8 defense: 2 experience_reward: 25 gold_reward: 10 }
This combat system demonstrates:
-
Dynamic health bars using CSS and template expressions
-
Conditional action availability based on resources (mana)
-
Bindings flowing down from scene to card
-
Dynamic properties
-
Event-driven gameplay with multiple outcomes
-
State management across multiple cards
-
Visual feedback through flash messages
Part 4: Advanced Features
Custom Elements and Mixins
As your games grow more complex, you’ll want to create your own element types and reusable components.
Creating Custom Elements
Use @elem
to define new element types based on existing ones:
@elem npc = actor @npc village_guard { name: "Guard" occupation: "Village Protector" dialogue: "Halt! State your business." }
This creates a new @npc
element type that inherits all the properties of @actor
but can have its own defaults and behaviors.
Adding Default Attributes
Use @defaults
to give custom elements standard attributes:
@elem monster = actor @defaults monster { hostile: true experience_reward: 10 gold_reward: 5 behavior_pattern: :aggressive } @monster goblin { name: "Goblin Warrior" health: 25 attack_power: 8 // Inherits: hostile: true, experience_reward: 10, etc. }
Mixins for Shared Behavior
Mixins let you share common attributes across different element types:
@mixin combat_capable { health: 100 max_health: 100 attack_power: 10 defense: 5 take_damage: function(amount) { this.health = Math.max(0, this.health - amount); return this.health <= 0; } heal: function(amount) { this.health = Math.min(this.max_health, this.health + amount); } } @elem warrior = actor<combat_capable> @elem guardian = actor<combat_capable> @warrior player_character { name: "Hero" // Automatically gets all combat_capable attributes and methods }
Multiple mixins can be combined:
@mixin magic_user { mana: 50 max_mana: 50 spell_power: 12 } @elem battle_mage = actor<combat_capable, magic_user>
Behavior Trees
For complex NPC AI and game logic, Rez includes a powerful behavior tree system. Behavior trees let you create sophisticated, reactive AI without writing complex JavaScript.
Basic Behavior Tree Concepts
Behavior trees are composed of nodes that either succeed or fail:
-
Composite nodes coordinate multiple child behaviors
-
Condition nodes test the game state
-
Action nodes modify the game state
@actor smart_guard { name: "Intelligent Guard" behaviors: ^[ $select [$sequence [player_nearby range=3] [guard_not_alerted] [set_alert_level level=1]] [$sequence [alert_level_equals level=2] [call_for_backup]] [patrol_route] ] }
Core Behavior Types
$select - Try children until one succeeds (OR logic):
behaviors: ^[ $select [has_weapon] // Try this first [find_weapon] // If no weapon, try to find one [flee_combat] // Last resort: run away ]
$sequence - All children must succeed (AND logic):
behaviors: ^[ $sequence [see_enemy] // First, spot an enemy [weapon_ready] // Then, make sure weapon is ready [attack_enemy] // Finally, attack ]
$parallel - Run multiple behaviors simultaneously:
behaviors: ^[ $parallel [patrol_area] // Keep patrolling [watch_for_threats] // While watching for danger ]
Writing Custom Behaviors
Create your own condition and action behaviors:
@behaviour player_nearby { options: ["range"] execute: function(owner, behaviour, wmem) { const range = behaviour.option("range"); const distance = owner.distanceTo($("player")); return { success: distance <= range, wmem: wmem }; } } @behaviour set_alert_level { options: ["level"] execute: function(owner, behaviour, wmem) { const level = behaviour.option("level"); owner.alert_level = level; return { success: true, wmem: wmem }; } }
Complex AI Example
Here’s a sophisticated NPC that reacts to the player and environment:
@actor tavern_keeper { name: "Bartender Bob" mood: :neutral reputation_with_player: 0 last_interaction: 0 behaviors: ^[ $select // Emergency responses [$sequence [tavern_on_fire] [evacuate_tavern]] // Player interactions [$sequence [player_at_bar] [$select [$sequence [player_reputation_high] [greet_warmly]] [$sequence [player_reputation_low] [greet_coldly]] [greet_neutrally]]] // Idle behaviors [$sequence [no_customers_present] [$select [clean_glasses] [restock_bottles] [count_money]]] // Default: serve customers [serve_random_customer] ] } @behaviour player_at_bar { execute: function(owner, behaviour, wmem) { const player_location = $("player").current_location; return { success: player_location === "tavern_bar", wmem: wmem }; } } @behaviour greet_warmly { execute: function(owner, behaviour, wmem) { const greetings = [ "Welcome back, friend! The usual?", "Good to see you again! What can I get you?", "My favorite customer! What brings you here today?" ]; owner.current_dialogue = greetings.randomElement(); $("game").trigger_event("npc_speaks", {npc: owner, text: owner.current_dialogue}); return {success: true, wmem: wmem}; } }
Custom Scripts and Styles
Extend Rez with your own JavaScript functions and CSS styling.
Adding Custom JavaScript
Use @script
to add utility functions:
@script { // Dice rolling utilities function rollDie(sides) { return Math.floor(Math.random() * sides) + 1; } function rollDice(count, sides, modifier = 0) { let total = modifier; for (let i = 0; i < count; i++) { total += rollDie(sides); } return total; } // Combat calculation helpers function calculateDamage(attacker, defender) { const baseDamage = rollDice(2, 6) + attacker.strength_modifier; const mitigatedDamage = Math.max(1, baseDamage - defender.armor_class); return mitigatedDamage; } }
Adding Custom CSS
Use @stylesheet
to style your game:
@stylesheet { .character-sheet { border: 2px solid #8B4513; border-radius: 10px; padding: 20px; background: linear-gradient(135deg, #F5E6D3, #E8D5B7); box-shadow: 0 4px 8px rgba(0,0,0,0.3); } .health-bar { width: 100%; height: 20px; background-color: #444; border: 2px solid #000; border-radius: 10px; overflow: hidden; } .health-fill { height: 100%; background: linear-gradient(90deg, #ff4444, #ffaa44, #44ff44); transition: width 0.3s ease; } .combat-actions button { background: #8B4513; color: white; border: none; padding: 10px 20px; margin: 5px; border-radius: 5px; cursor: pointer; font-size: 16px; } .combat-actions button:hover { background: #A0522D; transform: translateY(-2px); } .flash-message { background: #FFD700; border: 2px solid #FFA500; padding: 10px; margin: 10px 0; border-radius: 5px; text-align: center; font-weight: bold; } }
JavaScript Patches
Extend built-in JavaScript objects with the @patch
directive:
@patch { patch: "Array" method: "sample" impl: function(count = 1) { const shuffled = [...this].sort(() => 0.5 - Math.random()); return count === 1 ? shuffled[0] : shuffled.slice(0, count); } } // Now you can use: // ["red", "green", "blue"].sample() -> "green" // [1,2,3,4,5].sample(3) -> [3, 1, 5]
Custom Template Filters
Create your own template expression filters:
@filter DICE_ROLL_FILTER { name: "roll" impl: (dice_notation) => { // Parse "2d6+3" format const match = dice_notation.match(/(\d+)d(\d+)(?:\+(\d+))?/); if (!match) return 0; const count = parseInt(match[1]); const sides = parseInt(match[2]); const modifier = parseInt(match[3]) || 0; return rollDice(count, sides, modifier); } } // Use in templates: // <p>You rolled ${"2d6+3" | roll} damage!</p>
Building Modular Game Systems
For large games, organize your code into modular systems:
// combat_system.rez @script { const CombatSystem = { initiateCombat(player, enemy) { $("game").combat_state = { player: player, enemy: enemy, turn: "player", round: 1 }; return RezEvent.switchScene("combat"); }, processTurn(action, target) { const state = $("game").combat_state; // Process combat logic... return this.checkCombatEnd(state); }, checkCombatEnd(state) { if (state.player.health <= 0) { return RezEvent.playCard("player_defeated"); } else if (state.enemy.health <= 0) { return RezEvent.playCard("enemy_defeated"); } return RezEvent.render(); } }; } // inventory_system.rez @script { const InventorySystem = { addItem(character, item) { if (!character.inventory) character.inventory = []; character.inventory.push(item); this.sortInventory(character); }, removeItem(character, itemId) { if (!character.inventory) return false; const index = character.inventory.findIndex(item => item.id === itemId); if (index >= 0) { character.inventory.splice(index, 1); return true; } return false; }, sortInventory(character) { character.inventory.sort((a, b) => { // Sort by type, then by name if (a.type !== b.type) return a.type.localeCompare(b.type); return a.name.localeCompare(b.name); }); } }; }
Part 5: Technical Reference
How Rez Works Under the Hood
Understanding Rez’s compilation and runtime process helps you debug issues and optimize your games.
The Compilation Process
When you run rez compile
, several steps occur:
-
Parse Source Files - Rez reads your
.rez
files and parses the element definitions -
Validate References - Checks that element references (like
#player
) point to existing elements -
Generate JavaScript - Converts elements into JavaScript objects and classes
-
Template Compilation - Transforms template expressions into rendering functions
-
Asset Processing - Copies assets and generates asset manifests
-
Runtime Assembly - Combines everything into
runtime.js
andindex.html
Runtime Object Model
Your Rez elements become JavaScript objects at runtime:
-
@game
→RezGame
instance -
@scene
→RezScene
instance -
@card
→RezCard
instance -
@actor
→RezActor
instance -
And so on…
Each object gets methods and properties based on the attributes you defined:
@actor hero { name: "Brave Knight" health: 100 heal: function(amount) { this.health += amount; } } // Becomes at runtime: const hero = new RezActor({ id: "hero", name: "Brave Knight", health: 100, heal: function(amount) { this.health += amount; } });
The Rendering Pipeline
Content rendering follows this flow:
-
Game Layout renders with scene content in
${content}
-
Scene Layout renders with card content in
${content}
-
Card Content renders with template expressions resolved
-
Blocks (if any) render and provide additional template variables
Each level wraps the next in HTML structure:
<div class="game"> <!-- Game wrapper --> <!-- Game layout content --> <div class="scene" id="scene_forest"> <!-- Scene wrapper --> <!-- Scene layout content --> <div class="card" id="card_clearing"> <!-- Card wrapper --> <!-- Card content --> </div> </div> </div>
Memory and Performance
Global Objects: Elements with $global: true
become window variables:
@actor player { $global: true name: "Hero" } // Accessible as: window.$player or just $player
Template Caching: Template rendering functions are compiled once and cached for performance.
Event Delegation: Rez uses event delegation for clicks, so dynamically added content automatically works.
Complete Attribute Type Reference
Primitive Types
Boolean
visible: true completed: false enabled: yes // yes/no also work hidden: no
Number
count: 42 price: 19.99 temperature: -10 percentage: 0.85
String
name: "Magic Sword" description: "A blade that glows with inner light" note: "" // Empty string
Keyword
type: :weapon rarity: :legendary status: :active category: :consumable
Complex Types
Element Reference
owner_id: #player location_id: #forest_clearing target_id: #dragon
List (space-separated, no commas)
colors: [:red :green :blue] scores: [100 250 75 420] items: [#sword #shield #potion] mixed: ["text" 42 :keyword #reference]
Set (unique values, unordered)
tags: #{:magical :rare :weapon} visited_locations: #{#town #forest #cave}
Template (multi-line with expressions)
content: ``` <h2>${title}</h2> <p>Welcome, ${player.name}!</p> $if(player.level > 5) {% <p>You are an experienced adventurer.</p> %} ```
Binding List (key-value pairs)
bindings: [ player: #main_character, location: () => player.current_location, gold: &player.gold ]
Dynamic Types
Dynamic Initializer (runs once at game start)
random_stat: ^i{return Math.random() * 20 + 1;} starting_gold: ^i:1{return rollDice(3, 6) * 10;} // Priority 1 (runs early) total_stats: ^i:5{return this.str + this.dex + this.int;} // Priority 5
Dynamic Property (calculated each access, see Part 2 for basics)
// Advanced: Complex calculations with error handling spell_save_dc: ^p{ const base = 8 + this.proficiency_bonus; const modifier = this.spell_casting_modifier || 0; return Math.max(8, base + modifier); } // Advanced: Conditional logic and multiple dependencies movement_speed: ^p{ let speed = this.base_speed || 30; if (this.is_encumbered) speed = Math.floor(speed / 2); if (this.hasCondition?.("haste")) speed *= 2; if (this.hasCondition?.("slow")) speed = Math.floor(speed / 2); return Math.max(0, speed); } // Advanced: Expensive calculations that should be cached threat_assessment: ^p{ // This is expensive - consider using ^i{} with manual updates instead const nearby = this.getAllNearbyEnemies(); return nearby.reduce((threat, enemy) => { return threat + (enemy.level * enemy.aggression_factor); }, 0); }
Dice Notation
damage: ^r:2d6+3 // Roll 2 six-sided dice, add 3 treasure: ^r:d100 // Roll 1 hundred-sided die healing: ^r:1d4+1 // Roll 1 four-sided die, add 1
Probability Table
loot_type: |:common 60 :uncommon 25 :rare 10 :legendary 5| encounter: |#goblin 40 #orc 30 #troll 20 #dragon 10| weather: |"sunny" 50 "cloudy" 30 "rainy" 15 "stormy" 5|
Script Types
Event Script (arrow function, receives object and event)
on_use: (item, event) => { player.health += item.healing_power; return RezEvent.flash("You feel better!"); } on_enter: (scene, event) => { scene.visit_count = (scene.visit_count || 0) + 1; return RezEvent.render(); }
Action Script (regular function, this
is the owner object)
attack: function(target) { const damage = this.attack_power + rollDie(6); target.health -= damage; return damage; } canAfford: function(price) { return this.gold >= price; }
Behavior Tree
behaviors: ^[ $select [$sequence [health_low] [has_potion] [use_potion]] [$sequence [enemy_nearby] [attack_enemy]] [wander_around] ]
Special Attributes
System Attributes (control Rez behavior)
$global: true // Create global variable $template: true // Mark as template for copying $js_ctor: "CustomClass" // Use different JavaScript constructor $built_in: true // Mark as built-in (documentation only)
ID Suffix Attributes (create automatic references)
owner_id: #player // Also creates 'owner' property pointing to player object location_id: #forest // Also creates 'location' property pointing to forest object target_id: #dragon // Also creates 'target' property pointing to dragon object initial_card_id: #start // Also creates 'initial_card' property pointing to start card initial_scene_id: #main // Also creates 'initial_scene' property pointing to main scene parent_id: #container // Also creates 'parent' property pointing to container object
Best Practices and Patterns
Project Organization
Structure larger projects with clear separation:
src/ ├── game.rez # Main game definition ├── characters/ │ ├── player.rez # Player character │ ├── npcs.rez # Non-player characters │ └── monsters.rez # Enemy creatures ├── locations/ │ ├── town.rez # Town scenes and cards │ ├── dungeon.rez # Dungeon areas │ └── overworld.rez # World map ├── items/ │ ├── weapons.rez # Weapon definitions │ ├── armor.rez # Armor and protection │ └── consumables.rez # Potions, scrolls, etc. └── systems/ ├── combat.rez # Combat mechanics ├── inventory.rez # Inventory management └── dialogue.rez # Conversation system
Use the include macro to combine files:
// In game.rez @game { name: "My Adventure" // ... game definition } %(characters/player.rez) %(characters/npcs.rez) %(locations/town.rez) %(items/weapons.rez) %(systems/combat.rez)
Naming Conventions
Use consistent naming patterns:
// Scenes: describe location or context @scene forest_entrance @scene combat_arena @scene character_creation // Cards: describe content or action @card explore_clearing @card merchant_dialogue @card level_up_screen // Actors: role or name @actor village_guard @actor evil_wizard @actor shop_keeper // Items: descriptive names @item rusty_sword @item healing_potion @item magic_amulet
State Management
Keep game state organized and predictable:
@game { name: "Adventure Game" // Global game state current_chapter: 1 player_score: 0 game_flags: {} // Helper methods for state setFlag: function(name, value = true) { this.game_flags[name] = value; } getFlag: function(name) { return this.game_flags[name] || false; } addScore: function(points) { this.player_score += points; if (this.player_score >= 1000) { this.setFlag("high_score_achieved"); } } }
Error Handling
Handle edge cases gracefully:
@card examine_item { bindings: [ target_item: () => { const itemId = $("game").selected_item_id; return itemId ? $(itemId) : null; } ] content: ``` $if(target_item) {% <h3>${target_item.name}</h3> <p>${target_item.description}</p> <button event="use_item">Use Item</button> %}, {% <p>No item selected to examine.</p> <button card="inventory">Back to Inventory</button> %} ``` }
Performance Tips
-
Use Global Sparingly: Only mark frequently-accessed objects as global
-
Optimize Templates: Avoid complex calculations in template expressions
-
Choose the Right Dynamic Type: Match the attribute type to your use case
-
Cache Expensive Operations: Store calculated values rather than recalculating
-
Batch DOM Updates: Use
{render: true}
to refresh all content at once
Choosing Between Dynamic Types:
// Use ^p{} for values that change frequently and need to be current health_percentage: ^p{return this.health * 100 / this.max_health;} // Use ^i{} for complex setup that rarely changes starting_equipment: ^i{ return this.character_class === "warrior" ? ["sword", "shield", "armor"] : ["dagger", "lockpicks", "cloak"]; } // Use regular attributes for values that change manually experience: 0 // Updated by events, not calculated // Use ^i{} with manual updates for expensive calculations threat_level: ^i{return this.calculateThreatLevel();} recalculate_threat: function() { this.threat_level = this.calculateThreatLevel(); }
Template Optimization:
// Good: use dynamic properties to move calculations out of templates @actor player { base_attack: 10 strength: 15 attack_power: ^p{return this.base_attack + Math.floor(this.strength / 2);} } content: ``` Attack Power: ${player.attack_power} ``` // Less good: calculate every time template renders content: ``` Attack Power: ${player.base_attack + Math.floor(player.strength / 2)} ```