Custom Vue Features

Directives

v-persist

Keeps images in browser memory cache after they load. Prevents the browser from evicting decoded image data when elements are removed from the DOM (e.g., when closing a panel that uses v-if).

Usage:

<img :src="iconPath" v-persist />

Why it exists:

When a Vue component is destroyed (via v-if), all its <img> elements are removed from the DOM. The browser may then evict the decoded image data from memory. When the component is recreated, the browser needs to re-decode the image from disk cache, causing a brief visual delay.

v-persist creates a hidden JavaScript reference to each loaded image, telling the browser to keep the decoded data in memory.

Example - Item display component:

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

const ItemIcon = defineComponent({
  props: ['item'],
  setup(props) {
    const icon = computed(() => props.item.getTrait('image'));
    return { icon };
  },
  template: /*html*/`
    <img v-if="icon" :src="icon" class="item-icon" v-persist />
  `
});

When to use:

  • Images that appear in panels or screens that open/close frequently (character sheets, inventory, etc.)
  • Character portraits and doll layers
  • Any image that should always display instantly when its container is reopened

When not needed:

  • Images that are always visible (static backgrounds, persistent UI elements)
  • Images shown only once (splash screens, one-time animations)

The cache holds up to 600 images. When full, the oldest entry is removed to make room for new ones.


v-fit

Shrinks font size so text fits within its container without clipping. Reacts to text changes and container resizes automatically.

Usage:

<div v-fit>{{ characterName }}</div>
<div v-fit="{ min: 8 }">{{ longTitle }}</div>

Options:

OptionTypeDefaultDescription
minnumber6Minimum font size in px

Example - Character name badge:

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

const CharacterBadge = defineComponent({
  props: ['character'],
  setup(props) {
    const name = computed(() => props.character.getTrait('name'));
    return { name };
  },
  template: /*html*/`
    <div class="badge" v-fit>{{ name }}</div>
  `
});

When to use:

  • Name labels on fixed-width containers (character portraits, item slots)
  • Any single-line text that must not clip or overflow

When not needed:

  • Text that can wrap to multiple lines
  • Text in containers that grow to fit content

v-script

Renders DryadScript text on any element with full lore-link interactivity. Resolves the input through the engine's text pipeline by default and attaches the hover/click event delegation needed to open lore tooltip popups on [[record_id]] references. Use this anywhere you display engine-resolved text in your own templates — plain v-html will render visually but the lore links won't react to hover or click. See Lore & Encyclopedia for the full lore system.

Usage (string form):

<div v-script="rawText" />

Resolves rawText through the text pipeline (placeholders, if{}, [[lore-links]], etc.) and renders it. Equivalent to v-script="{ html: rawText }".

Usage (object form):

<div v-script="{ html, resolver, navMode, onNavigate, disabled }" />
OptionTypeDefaultDescription
htmlstringrequiredtext to render
resolverbooleantruerun html through game.resolveString(html, true).output (noExecuteActions=true; rendering must never fire side effects). Set false if html is already resolved upstream.
navModebooleanfalsefor in-place navigation (e.g., the Encyclopedia tab): clicks call onNavigate(recordId) instead of opening a popup. Hover is suppressed in this mode.
onNavigate(recordId: string) => void—callback for navMode clicks
disabledbooleanfalsesuppress all hover/click handling. Use during DOM-unstable phases like a typing animation that re-parses HTML each frame, so the popup doesn't latch onto a stale anchor.

Example - choice description:

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

const ChoiceCard = defineComponent({
  props: ['choice'],
  template: /*html*/`
    <div class="choice">
      <h3>{{ choice.name }}</h3>
      <div v-if="choice.description" v-script="choice.description" class="choice-description"></div>
    </div>
  `
});

Reactivity caveat: the directive re-renders only when its bound value changes. Reactive dependencies inside resolveString (e.g., a placeholder that reads a ref the player can change mid-session) won't trigger a re-render automatically. For those cases, wrap in your own computed(() => game.resolveString(text).output) and pass with { resolver: false }.

When to use:

  • Any custom component that displays user-authored text containing [[record]] references, |placeholders|, or other DryadScript syntax
  • Choice descriptions, item descriptions, status descriptions in plugin UIs

When not needed:

  • Plain text without any DryadScript syntax — use {{ text }} interpolation instead.

v-popover

Floating UI based hover popover that just works on any element.

Usage (string form — HTML rendered via v-script):

<button v-popover="'<b>Strength</b>: raises [[stat_attack]]'">Stats</button>

The string is treated as DryadScript HTML and rendered through v-script, so [[record]] lore-links, |placeholders|, and if{} blocks all work inside the popover.

Usage (object form — html mode):

<div v-popover="{ html: skill.description, width: 400 }">{{ skill.name }}</div>

Usage (object form — Vue component mode):

<div v-popover="{ component: ItemCard, props: { item }, width: 360 }">
  <img :src="item.icon">
</div>

When component is supplied it wins over html. The component receives props via v-bind.

Placement modifiers (sugar for the placement option):

<div v-popover.bottom="content">Below</div>
<div v-popover.right="content">To the right</div>

Equivalent to v-popover="{ ..., placement: 'bottom' }". The default placement is 'top'. Floating UI's flip() and shift() middleware automatically rescue placement near viewport edges.

Binding shape:

FieldTypeDefaultDescription
htmlstring—content to render through v-script
componentComponent—Vue component to render inside (wins over html)
propsobject{}props passed to component via v-bind
widthnumber | string'300px'popover width; numbers become ${n}px
placementPlacement'top'any Floating UI placement (top, bottom-start, left-end, etc.)

Hover handoff:

The popover sits at offset: 0 against the target side, and a 100 ms close-debounce covers the cursor's transition between target and popover. Hovering the popover keeps it open; leaving the popover closes it after 100 ms.

Example — character status with rich popover content:

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

const InventoryRow = defineComponent({
  props: ['item'],
  setup(props) {
    return { item: props.item, ItemCard };
  },
  template: /*html*/`
    <div class="row" v-popover="{ component: ItemCard, props: { item }, placement: 'right' }">
      <img :src="item.icon">
      <span>{{ item.name }}</span>
    </div>
  `
});

When to use:

  • Rich hover-over information panels (item details, ability tooltips, character cards)
  • Anywhere you'd reach for a tooltip but need clickable content inside
  • Any place game text with [[lore-links]] should appear on hover

When not needed:

  • Plain text tooltips — use v-tooltip (PrimeVue), it's lighter

v-dragscroll

Enables drag-to-scroll on any scrollable container. Click and drag to scroll horizontally, vertically, or both.

Usage:

<!-- Horizontal only -->
<div class="scroll-container" v-dragscroll.x>...</div>

<!-- Vertical only -->
<div class="scroll-container" v-dragscroll.y>...</div>

<!-- Both directions (default) -->
<div class="scroll-container" v-dragscroll>...</div>

When to use:

  • Horizontal card/character lists that overflow their container
  • Any scrollable area where drag-to-scroll improves UX (especially touch devices)

When not needed:

  • Containers that don't overflow
  • Areas where drag conflicts with other interactions (text selection, sliders)

Powered by vue-dragscroll.


Quick Reference

DirectiveElementPurpose
v-persist<img>Keep loaded images in browser memory cache
v-fitAnyShrink font size so text fits without clipping
v-scriptAnyRender DryadScript text with [[lore-link]] interactivity
v-popoverAnyHover popover with HTML or Vue-component content (Floating UI)
v-dragscrollAnyDrag-to-scroll on scrollable containers
v-tooltipAnyShow tooltip on hover (from PrimeVue)