Vue 3 - Modern Way to Build Complex UI

Dryad Engine is developed with Vue 3, a modern JavaScript framework for building user interfaces. This means all the engine's UI components - from character sheets to inventory screens to dialogue overlays - are Vue components that you can extend, replace, or build upon.

The engine exposes Vue and its APIs through window.engine, so you can create custom components that integrate seamlessly with the existing UI without setting up any build tools.


What is Vue?

Vue is a framework that makes building interactive UIs simple and organized. Instead of manually manipulating HTML elements, you describe what the UI should look like based on your data, and Vue keeps everything in sync automatically.

Key concepts:

ConceptWhat it does
ComponentsReusable UI building blocks (buttons, panels, entire screens)
Reactive dataWhen data changes, the UI updates automatically
TemplatesHTML-like syntax to describe your component's structure
PropsPass data from parent to child components

Learning resources:

You don't need deep Vue knowledge to customize Dryad Engine - the patterns shown here will cover most use cases.


Creating Custom Components

Components are defined using defineComponent():

const { game, vue, primeVue } = window.engine;

// Import ref and defineComponent from vue to use in components
const { ref, defineComponent } = vue;

const MyComponent = defineComponent({
  // Register other components to use in template
  components: {
    Button: primeVue.Button
  },
  setup() {
    const message = ref("Hello!");

    function onClick() {
      message.value = "Clicked!";
    }

    return { message, onClick };
  },
  template: /*html*/`
    <div class="my-component">
      <p>{{ message }}</p>
      <Button label="Click me" @click="onClick" />
    </div>
  `
});

Props - Passing Data to Components

Props let parent components pass data to children. The engine's CharacterFace component uses a character prop:

const { vue, components, game } = window.engine;
const { defineComponent } = vue;
const { CharacterFace } = components;

// create a custom CharacterCard component that utilizes the built-in CharacterFace component
const CharacterCard = defineComponent({
  // Register engine's CharacterFace component to use in template
  components: { CharacterFace },
  // Declare what props this component accepts
  props: ['character'],
  setup(props) {
    // Access props.character in setup
    const name = props.character.getName();
    return { name };
  },
  template: /*html*/`
    <div class="character-card">
      <!-- Pass character prop to CharacterFace -->
      <CharacterFace :character="character" />
      <span>{{ name }}</span>
    </div>
  `
});

// Usage: pass a character object as prop
const alice = game.getCharacter("alice");
// In another component's template: <CharacterCard :character="alice" />

Anatomy of a Component

PartPurpose
setup()Where you define reactive data, functions, and logic
templateHTML structure that displays your data
componentsRegister other components to use inside this one
propsDeclare data this component receives from its parent

Reactive Data

Reactive data means the UI automatically updates when the data changes. In Dryad Engine, game objects are already reactive - characters, inventories, party, flags, and states are Vue reactive objects under the hood. When you mutate them, the UI updates automatically:

setup() {
  // Game objects are reactive - pass them to template directly without the need to wrap in ref() or reactive()
  const mc = game.getCharacter("mc");
  const party = game.getParty();

  // When mc or party change, the template updates automatically
  return { mc, party };
}
<!-- In template: access reactive properties directly -->
<p>Health: {{ mc.getResource("health") }} / {{ mc.getStat("health") }}</p>
<p>Name: {{ mc.getTrait("name") }}</p>

Use ref() for your own local component state:

const { ref } = vue;

setup() {
  // Use ref() for local UI state you create
  const isMenuOpen = ref(false);

  function toggleMenu() {
    isMenuOpen.value = !isMenuOpen.value;
  }

  return { isMenuOpen, toggleMenu };
}

Use computed() for derived values:

const { computed } = vue;

setup() {
  const mc = game.getCharacter("mc");

  // Computed recalculates when dependencies change
  const healthPercent = computed(() => {
    const current = mc.getResource("health");
    const max = mc.getStat("health");
    return Math.round((current / max) * 100);
  });

  return { mc, healthPercent };
}

Using Engine Components

The engine exports reusable components through window.engine.components:

const { vue, components, game } = window.engine;
const { defineComponent } = vue;
const { CharacterFace, CharacterDoll, BackgroundAsset } = components;

Example - Custom panel showing party faces:

const { vue, components, game } = window.engine;
const { defineComponent } = vue;
const { CharacterFace } = components;

const PartyFaces = defineComponent({
  components: { CharacterFace },
  setup() {
    const party = game.getParty();
    return { party };
  },
  template: /*html*/`
    <div class="party-faces">
      <div v-for="char in party" :key="char.id" class="face-slot">
        <CharacterFace :character="char" />
        <span>{{ char.getName() }}</span>
      </div>
    </div>
  `
});

The Slot System

Dryad Engine uses a slot system to organize where components appear in the UI. Think of slots as designated areas where you can plug in your own components - like LEGO bricks snapping into place.

Use game.addComponent() to register your component into a slot:

game.addComponent({
  id: "my-component-id",
  slot: "slot-name",
  title: "Display Title",
  component: MyComponent,
  order: 1  // Optional: controls position (lower = earlier)
});

State-Based Slots

State-based slots show one component at a time based on a state value. The slot renders whichever component's id matches the current state.

Example - Custom game state:

const { vue, game } = window.engine;
const { defineComponent } = vue;

const BattleScreen = defineComponent({
  setup() {
    function exitBattle() {
      game.setState("game_state", "exploration");
    }
    return { exitBattle };
  },
  template: /*html*/`
    <div class="battle-screen">
      <h1>Battle!</h1>
      <button @click="exitBattle">Retreat</button>
    </div>
  `
});

game.addComponent({
  id: "battle",           // When game_state = "battle", this shows
  slot: "game_state",
  component: BattleScreen
});

// Trigger it from anywhere:
game.setState("game_state", "battle");

Injection Slots

Injection slots render all registered components together. They act as extension points throughout the UI - tabs, toolbars, panels, and more.

See Component Slots for the full list of available slots and their default components.


Example: Adding a Character Tab

Add a custom tab to the character sheet that shows alongside Stats, Skills, and Inventory:

const { vue, game } = window.engine;
const { defineComponent, computed } = vue;

const NotesTab = defineComponent({
  setup() {
    // Get the currently viewed character
    const character = computed(() => {
      const charId = game.getState("selected_character");
      return game.getCharacter(charId);
    });

    // Store notes in a game store for persistence
    const notesStore = game.createStore("character_notes");

    const currentNote = computed(() => {
      if (!character.value) return "";
      return notesStore.get(character.value.id) || "";
    });

    function saveNote(event) {
      if (character.value) {
        notesStore.set(character.value.id, event.target.value);
      }
    }

    return { character, currentNote, saveNote };
  },
  template: /*html*/`
    <div class="notes-tab">
      <h3>Notes for {{ character?.getName() }}</h3>
      <textarea
        :value="currentNote"
        @input="saveNote"
        placeholder="Write notes about this character..."
      ></textarea>
    </div>
  `
});

game.addComponent({
  id: "notes",
  slot: "character-tabs",
  title: "Notes",
  component: NotesTab,
  order: 4  // After inventory (which is 3)
});

The tab appears automatically in the character sheet navigation.


Example: Adding a Toolbar Button

buttons

Add a custom button to the navigation toolbar:

const { vue, primeVue, game } = window.engine;
const { defineComponent } = vue;

const QuickSaveButton = defineComponent({
  components: {
    Button: primeVue.Button
  },
  setup() {
    function quickSave() {
      game.saveGame("quicksave");
      game.showNotification("Game saved!");
    }

    return { quickSave };
  },
  template: /*html*/`
    <Button
      icon="pi pi-save"
      @click="quickSave"
      v-tooltip.bottom="'Quick Save'"
    />
  `
});

game.addComponent({
  id: "toolbar-quicksave",
  slot: "navigation-toolbar",
  component: QuickSaveButton,
  order: 10
});

Removing or Replacing Components

Remove a default component:

game.removeComponent("default-inventory-header");

Replace by using the same id:

game.addComponent({
  id: "stats",  // Same ID as default stats tab
  slot: "character-tabs",
  title: "Character Stats",
  component: MyCustomStatsTab  // Your replacement
});

Quick Reference

I want to...Do this
Import Vue helpersconst { ref, computed, defineComponent } = vue
Create reactive valueconst x = ref(initialValue)
Create computed valueconst x = computed(() => ...)
Use engine componentconst { CharacterFace } = components
Add component to slotgame.addComponent({ id, slot, component })
Remove componentgame.removeComponent("component-id")
Change state-based slotgame.setState("state_name", "component_id")

Next Steps