Rez v1.6.0 — open source engine for making procedural narrative games

Another little component example:

I’m kind of experimenting with this style which I’ve seen in other games.

The <.dialog> component uses a couple of attributes dialog_tag and dialog_color, added to the actors, as well as their existing name attribute so it can style & attribute the dialog without a lot of noise:

<.dialog char={player}>Can I help you?</.dialog>
<.dialog char={gangster}>Where's the cash?</.dialog>

Looks pretty neat to my eye.

@styles {
  .dialog-box {
    margin-bottom: 1.5rem;
  }

  .speaker-name {
    display: inline-block;
    background: #485fc7;
    color: white;
    padding: 0.5rem 1rem;
    border-radius: 4px 4px 0 0;
    margin-bottom: 0;
  }

  .dialog-content {
    background: #f5f5f5;
    border: 1px solid #dbdbdb;
    border-radius: 0 4px 4px 4px;
    padding: 1.25rem;
  }
}

@component dialog (bindings, assigns, content) => {
  const char = assigns["char"];
  const tag = char.dialog_tag ? ` (${char.dialog_tag})` : "";
  const color = char.dialog_color || "#485fc7";
  return `<div class="dialog-box"><p class="speaker-name is-size-5" style="background: ${color}">${char.name}${tag}</p><div class="dialog-content">${content}</div></div>`;
}

@card c_player_speaks {
  bindings: [
    player: #player
    gangster: #mob_boss
  ]
  content: ```
  <.dialog char={player}>Can I help you?</.dialog>
  <.dialog char={gangster}>Where's the cash?</.dialog>
  ```
}

@player {
  dialog_color: "#485fc7"
  dialog_tag: "You"
}

@actor mob_boss {
  dialog_color: "#c3195d"
  dialog_tag: "Mob Boss"
}

As an aside this use of components makes use of the attr={expr} syntax for passing dynamic expressions to components that I totally pinched from Phoenix LiveView.

1 Like

I’ve just released v1.6.10.

It removes the Heredoc and Tracery grammar attribute types. In practice they were never used. It also removes the bundled tracery.js.

But the main change is that user components can now accept arbitrary expressions for attributes. For example:

<.buy cost={2+2} />

Would pass an assigns map of {cost: 4} to the buy component. All in-scope bindings are also available. So

@card c_test {
  bindings: [
    player: #player
  ]
  content: ```
  <.say actor={player}>I win!</.say>
  ```
}

Works as expected.

I worry for your cormorant …

1 Like

I decided I wanted to use some of the Heroicons in my game. I wrote a component that references an icon and makes it a clickable event generator:

@component icon (bindings, assigns, content) => {
  const icon_name = assigns["icon"];
  const event = assigns["event"];
  const svg = $heroicons.getAttributeValue(icon_name);

  if(event == undefined) {
    return `<span class="heroicon">${svg}</span>`;
  } else {
    // When there is an event, process all other assigns as data attributes
    let dataAttributes = '';

    // Iterate through assigns and convert them to data attributes
    // Skip the 'icon' and 'event' attributes as they're handled specially
    for (const [key, value] of Object.entries(assigns)) {
      if (key !== 'icon' && key !== 'event') {
        dataAttributes += ` data-${key}="${value}"`;
      }
    }

    // Create the anchor element with the event and all other data attributes
    return `<span class="heroicon"><a data-event="${event}"${dataAttributes}>${svg}</a></span>`;
  }
}

So now I can write the following:

<.icon name="arrow-path" event="randomize" />

and I get a nice clickable icon!

Screenshot 2025-03-12 at 17.47.21

Works really well but the component depends on this object that contains the SVG for the icons I want to use:

@object heroicons {
  $global: true

  arrow_path: ```
  <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
    <path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
  </svg>
  ```
}

I mean, it’s not terrible but I don’t love it. The Heroicons download has SVG files for each icon and I have the @asset tag. Can I do better?

Well I made a small change to the Rez compiler introducing a new system attribute $inline for @asset tags. When $inline is true:

@asset heroicon_arrow_path {
  $inline: true
  file_name: "arrow_path.svg"
}

Instead of copying the asset file into the dist/assets folder as normal the Rez compiler reads the asset content directly into a content string attribute so you can access it via:

$("heroicon_arrow_path").content

I put arrow-path.svg into the assets/icons folder of my game and the <.icon> component now looks for the related @asset with its inlined SVG content.

I still have to manually create an @asset element for each icons that I want but it’s not too onerous and maybe I can find a better way in the future.

1 Like

Actually the “something better” turned out to be super simple. I wrote a Ruby script, compile_assets that I use as follows:

./compile_assets assets/icons --prefix heroicons --inline > src/heroicon_assets.rez

The script automatically generates the @asset definitions for each file in the assets/icons folder, I just redirect the output to src/heroicons_assets.rez:

@asset heroicons_arrow_path_rounded_square {
  $inline: true
  file_name: "arrow-path-rounded-square.svg"
}

@asset heroicons_arrow_path {
  $inline: true
  file_name: "arrow-path.svg"
}

Now my heroicons.rez library is:

%(heroicon_assets.rez)

@styles {
  .heroicon {
    display: inline-block;
    vertical-align: middle;
    width: 1.5rem;
    height: 1.5rem;
  }
}

@component icon (bindings, assigns, content) => {
  const icon_name = assigns["icon"];
  const icon = $(`heroicon_${icon_name}`);
  const event = assigns["event"];

  if(event == undefined) {
    return `<span class="heroicon">${icon.content}</span>`;
  } else {
    // When there is an event, process all other assigns as data attributes
    let dataAttributes = '';

    // Iterate through assigns and convert them to data attributes
    // Skip the 'icon' and 'event' attributes as they're handled specially
    for (const [key, value] of Object.entries(assigns)) {
      if (key !== 'icon' && key !== 'event') {
        dataAttributes += ` data-${key}="${value}"`;
      }
    }

    // Create the anchor element with the event and all other data attributes
    return `<span class="heroicon"><a data-event="${event}"${dataAttributes}>${icon.content}</a></span>`;
  }
}

If I want to use another icon I copy its SVG file into the assets/icons folder and re-run the compile_assets script. Simples!

Here’s my script:
compile_assets.txt (2.1 KB)

2 Likes

My adventures with Twine taught me that Rez needed to have 1st class asset management baked in. It’s not like you can’t use assets in Twine or even that it’s that difficult — but it felt like kind of an afterthought.

That’s why, from the beginning, Rez has had the @asset element. Simply writing:

@asset heroicon_arrow_path {
  file_name: "arrow_path.svg"
}

Does 3 things:

  • It locates the asset where it is under the project assets folder and reports an error if it can’t be found. You can structure project assets any way you like and it will find it. The corollary is that assets have to have a unique file name.
  • When the game is compiled all referenced non-inlined assets are copied into the dist/assets folder ready to be distributed with the game. They’re never missing or out of date. (Inlined assets have their contents read into a content attribute).
  • Ensures that, while you can always refer to an asset file as /assets/file_name.ext (since you know its name is unique, and all assets are packed into the assets folder), you can also use the $dist_path attribute and not concern yourself with hand-coding paths at all.

For example:

@asset canaletto_regata_1 {
  file_name: 'canaletto_venice_a_regatta_on_the_grand_canal.jpg'
  width: 5888
  height: 3760
}

@component img (bindings, assigns, content) => {
  const asset = $(assigns["name"]);
  const path = asset.$dist_path;
  const w = asset.width;
  const h = asset.height;
  return `<img src="${path}" width="${w}" height="${h}" />`;
}

@card c_card {
  content: ```
    While in Venice…
    <.img name="canaletto_regata_1" />
  ```
}

I should probably put that component into the stdlib as it’s so handy.

At the moment adding width & height for visual assets is manual but I’m thinking about integrating ExImageInfo and letting the compiler embed the metadata.

1 Like

I’m a little rusty with the way things work now, but at one time I found it handy to have higher resolution images displaying at smaller fixed dimensions. It allowed scaling of the interface/content while displaying the highest quality possible (as opposed to stretching and blurring the image). With high ppi displays, I think this is more useful now than ever.

I suppose content could have a CSS style that overrides the inherent pixel dimensions. Anyway, I think what you are doing to handle external media is very much needed. This is a great addition! :slight_smile:

1 Like

Even if I did implement such a feature it wouldn’t preclude you still setting the width and height attributes to whatever you want.

Another approach would be the same as I used for the Heroicons, a script to generate the @asset definitions. It could start with built-in info and then you just customise it. I guess one wrinkle with that approach might be that your custom definitions could get overwritten when you re-run the script. Probably it would need to be sophisticated enough to do the right thing under those circumstances.

1 Like

One small change I have made is to introduce a little bit of sugar so that instead of writing:

@asset blah_blah {
  file_name: "blah_blah.png"
}

you can write:

@auto_asset {
  file_name: "blah_blah.png"
}

and it will automatically assign an id of asset_blah_blah.

That is it automatically assigns an id which is base name of the asset file with an asset_ prefix.

This will break down in a case where you have asset files that only differ in their extension. At the moment this seems unlikely. Of course I will regret this statement.

1 Like