Scripter

Scripter is a plugin for Virt-A-Mate that allows you to write javascript to connect the different plugins and atoms of your scenes. It’s a core component of Voxta, it’s what allows actions to affect objects and trigger events in Virt-A-Mate.

Basic Setup

Here is the basic setup we use in the Beta Scene.

index.js (your scene logic):

import { scripter, scene } from "vam-scripter";

// To keep the script simple, we moved most of the plumbing code to lib1.js
import { initVoxta } from "./lib1.js";

// This is the Person atom. You could also use scripter.containingAtom to get the atom where the script is attached.
const person = scene.getAtom("Person");
// This is the Timeline instance we want to control
const timeline = person.getStorable("VamTimeline.AtomPlugin");
// This is VAMOverlays, to show subtitles
const overlays = scene.getAtom("Overlays").getStorable("VAMOverlaysPlugin.VAMOverlays");
// This is the action storable used to show subtitles
var setSubtitles = overlays.getStringParam("Set and show subtitles");
// This is to switch color based on who is talking
var subtitlesColor = overlays.getColorParam("Subtitles Color");

// Let's initialize Voxta (see lib1.js)
const voxta = initVoxta({
    atom: scripter.containingAtom.getStorable("Voxta")
});

// This is when the state changes (idle, thinking, listening, speaking)
voxta.onStateChanged = state => {
    // We play the animation matching the state, such as voxta_state_idle
    timeline.invokeAction("Play voxta_state_" + state);

    // When the AI is thinking, it means the user just spoke. We pring the user's last message.
    if(state == 'thinking') {
        subtitlesColor.val = '#90CDE0';
        setSubtitles.val = voxta.getLastUserMessage();
    }
};
    
// This is when there is an action inference
voxta.onAction = action => {
    // We simply play an animation that matches the name
    timeline.invokeAction("Play voxta_action_" + action);
};

// Whenever Voxta speaks, we show the subtitles
voxta.onVoxtaCharSpeak = message => {
  console.log('speak ' + message);
  subtitlesColor.val = '#E5BEBE';
  setSubtitles.val = message;
};

lib1.js (generic voxta integration script):

import { scene, scripter } from "vam-scripter";

const that = {};

let voxtaState;
let voxtaUserMessage;
let voxtaCharacterMessage;
let voxtaCurrentAction;

// We make a simple function that creates the actions we need to map, it's just there to simplify the main script.
export function initVoxta(params) {
  const voxta = params.atom;
  voxtaState = voxta.getStringChooserParam("State");
  voxtaUserMessage = voxta.getStringParam("LastUserMessage");
  voxtaCharacterMessage = voxta.getStringParam("LastCharacterMessage");
  voxtaCurrentAction = voxta.getStringParam("CurrentAction");

  scripter.declareAction("OnVoxtaStateChanged", () => {
    try {
      if(that.onStateChanged != undefined) that.onStateChanged(voxtaState.val);
    } catch (e) {
      console.log(e);
    }
  });

  scripter.declareAction("OnVoxtaCharSpeak", () => {
    try {
      if(that.onVoxtaCharSpeak != undefined) that.onVoxtaCharSpeak(voxtaCharacterMessage.val);
    } catch (e) {
      console.log(e);
    }
  });

  scripter.declareAction("OnVoxtaAction", () => {
    try {
      if(that.onAction != undefined) that.onAction(voxtaCurrentAction.val);
    } catch (e) {
      console.log(e);
    }
  });
  
  that.getState = () => {
    return voxtaState.val;
  };

  that.getLastUserMessage = () => {
    return voxtaUserMessage.val;
  };

  that.getLastCharacterMessage = () => {
    return voxtaCharacterMessage.val;
  };

  return that;
}

Tips

Animations variety

You can call one of many Timeline actions by appending /* to the animation names. For example, Play voxta_state_idle/* will play any animations that starts with voxta_state_idle/.

Queuing animation

import { scripter, scene } from "vam-scripter";

// Let's define a pendingAction variable
let pendingAction = undefined;

// ... variables, initVoxta...

voxta.onStateChanged = state => {
    // ... this code does not change

    if(state == 'thinking') {
        // ... this code does not change
    } else if(state == 'idle' && (!!pendingAction) ) {
        // Here we check if there is a pending action and run it as soon as we're idle
        // In this case we invoke a Timeline animation
        timeline.invokeAction("Play voxta_action_" + pendingAction);
        pendingAction = undefined;
    }
};

voxta.onAction = action => {
    // We need to determine a rule for actions we don't want to run right away
    // For example, here if it starts with 'do_', we queue it
    const shouldQueue = action.startsWith("do_");
    if(!shouldQueue || voxta.getState() == 'idle') {
        timeline.invokeAction("Play voxta_action_" + action);
    } else if(shouldQueue) {
        pendingAction = action;
        console.log('Queuing ' + action + '...');
    }
};

Prevent interruptions

A similar example would be to prevent actions from interrupting, and simply drop them. Simply avoid setting pendingAction and the action will not do anything.

Creating custom logic to determine whether to send a message in Voxta

You might have cases where you want to invoke a trigger message, but not necessariliy interrupt the character. For example, when you look at an object for some time, you might want to let the AI know.

A way to achieve that is to move the logic you want in a separate Scripter instance with only the logic you needs.

import { scripter } from "vam-scripter";

// We get the Voxta instance, could also be on another atom using scene.getAtom("AtomName")
const voxta = scripter.containingAtom.getStorable("Voxta");
// We need to get the state
const state = voxta.getStringChooserParam("State");
// And the trigger message action param
const triggerMessage = voxta.getStringParam("TriggerMessage");

// This is our custom trigger
const triggerMessageIfIdle = scripter.declareStringParam({
    "name": "TriggerMessageIfIdle",
    "default": ""
});
triggerMessageIfIdle.onChange(val => {
    // The logic here is simply to ignore the message unless we're idle
  if(state.val == 'idle') triggerMessage.val = val;
  triggerMessageIfIdle.valNoCallback = '';
});