CYOA

CYOA

  • Docs
  • API
  • Blog

API

Feature List Overview

  • Fully scriptable play experience. Start/stop/seek video files based on user input.
  • Storage to keep track of past playthroughs, or to save state.
  • Video overlays, so you can layer video with transparent backgrounds.
  • Send messages to the player.
  • Collect information from the player.
  • Register callbacks for when a player reaches a certain time index.
  • Display action buttons for users to click (or press an associated key).
  • Advanced Game mode which allows for much more custom UI handling.
  • Need something else? Ask here.

Getting Started

As part of every cyoa project is a package yaml file. This file contains all of the information about your project, describing all of the script files your project uses, as well as the filename of every media file that you will want to load.

Here's an eample package.yml file.

name: My First CYOA Adventure
author: ftrees
scripts:
  - index.js
videos:
  - intro.mp4
  - finale.mp4
# The following are optional:
#
# Use this if your game is an update to another game
# parent:
#   hash:
#   name:
#
# Use this if your game requires more parts than just the parent
# required:
#   - hash:
#     name:

API

The api is promise-based. It uses a library called postmessage-rpc, which is already set up for project to use when it loads.

NOTE: For apis that do not return a value, or return a value you don't care to wait for, you should pass false as the 3rd argument to the rpc.call function.

load

Load a video and start playback.

  • video - (string) Name of the video to load (matches declared file in package.)
  • overlay - (string) Name of the overlay video
  • index - (number) time index, in seconds, to start playing at ex: 123.456
  • overlayidx - (number) time index to start at for the overlay
  • loop - (boolean) if you want the video to loop when it reaches the end
  • beats - (string) name of beat file to load (matches declared file in package.)
  • beatData - (array) of beat data. Don't use both beats and beatData
  • barFile - (string) name of the bar file to load (matches declared file in package.)
  • barData - (array) of bar data. Don't use both barFile and barData.

example

rpc.call('load', { video: 'demo.mp4', overlay: 'test.webm' }, false);

pause

Pause playback of all videos.

example

rpc.call('pause', false);

play

Start playback of all videos from current time indices.

  • loop - (boolean) if you want the video to loop when it reaches the end

example

rpc.call('play', { loop: true }, false);

seek

Seek to time index.

  • index - (number) time index, in seconds, to start playing at ex: 123.456
  • overlayidx - (number) time index to start at for the overlay
  • delta - (number) time, in seconds, change by ex: -123.456 (backwards) or 2 (forwards)
  • overlaydelta - (number) in seconds, change by for the overlay

example

rpc.call('seek', { index: 0, overlayidx: 0 }, false)
rpc.call('seek', { delta: 2 }, false) // skip ahead 2 seconds

listener

You can also listen for when the video was seeked to. This is helpful when you are seeking by a delta and you want to know the result time index of the seek.

rpc.expose('seek', ({ index, overlayidx, was, overlaywas }) => {
  // index is the new video index
  // overlayidx is the new overlay index
  // was is the time before seek
  // overlaywas is the time before seek
});

when

Resolve a promise when the time index is reached or has been passed in the main video

  • index - (number) time index, in seconds, to start playing at ex: 123.456 Returns:
  • A promise for when the when is fired.

example

// notice that we do not pass in `, false` at the end.
// this tells the rpc we want a callback
rpc.call('when', { index: 60 }).then(() => {
  // time index 60 has been reached / passed
})

prompt

Propmt a user and resolve a promise with the choice a user has selected.

  • id - (string) A unique id for the prompt
  • title - (string) Text for the title of the prompt
  • content - (string | 'Array<form elements>) The contnet of the prompt
  • choices - (Array<string>) Button text Returns:
  • A promise with the result of the prompt

example

// notice that we do not pass in `, false` at the end.
// this tells the rpc we want a callback
rpc.call('prompt', {
  id: 'ask',
  title: 'You can ask for an answer...',
  content: 'Are you tired?',
  choices: ['Yes', 'No'],
}).then(({ choice }) => {
  // choice: 'Yes' || 'No'
});

form element example

rpc.call('prompt', {
  id: 'form',
  title: 'This is a form',
  content: [
    { type: 'text', value: 'This is plain text.' },
    { type: 'textbox', name:'name', label: 'What is your name?' },
    {
      type: 'radio',
      description: 'What are you?',
      name: 'sex',
      value: '',
      labels: [ 'Male', 'Female', 'Other' ],
      values: [ 'male', 'female', 'other' ],
    },
    {
      type: 'radio',
      description: "Can't change me",
      name: 'ha',
      value: 'chosen',
      labels: ['Chosen', "Can't pick me"],
      values: ['chosen', 'cant'],
      disabled: [false, true],
    },
    {
      type: 'radio',
      description: "Can't pick me",
      name: 'haha',
      value: 'chosen',
      labels: ['Unpickable', "Can't pick me"],
      values: ['cant', 'alsocant'],
      disabled: true,
    },
    {
      type: 'check',
      description: 'How are you?',
      name: 'feeling',
      labels: ['Hungry', 'Sleepy', 'Thirsty'],
      value: [false, false, false],
      disabled: true, // can work the same way as radio (see above)
    },
    { type: 'textbox', name: 'about', label: 'Tell me about yourself', multiline: true },
  ],
  choices: ['OK', 'Cancel'],
}).then(({choice, values}) => console.dir({ choice, values }));

form validation example

rpc.expose('required_endpoint', ({ values, options }) => {
  // This uses a yup schema for validation.
  // See: https://github.com/jquense/yup
  // Fill out the schema and validation here
  // Take care to always return the validate(values, options) from the passed in args
  return yup.object({
    name: yup.string().required('You must have a name'),
    sex: yup.string().required('WHAT ARE YOU?!'),
    about: yup.string().required('You must tell me about yourself'),
  }).validate(values, options);
});

rpc.call('prompt', {
  id: 'required',
  title: 'These are required',
  choices: [{ text: 'Cancel', type: 'cancel' }, 'OK'],
  validation: 'required_endpoint', // exposed endpoint, see above
  content: [
    { type: 'textbox', name: 'name', label: 'What is your name?', required: true, value: 'Bob' },
    {
      type: 'radio',
      description: 'What are you?',
      name: 'sex',
      value: '',
      labels: ['Male', 'Female', 'Other'],
      values: ['male', 'female', 'other'],
      required: true, // This is really only to show the * to indicate required on the field label
    },
    {
      type: 'textbox',
      name: 'about',
      label: 'Tell me about yourself',
      multiline: true,
      required: true, // This is really only to show the * to indicate required on the field label
    },
  ],
}).then(({ choice, values }) => {
  console.dir({ choice, values });
});

alert

Show an alert to the user with a timeout

  • message - (string) The alert message
  • duration - (number) The duration to show the message in milliseconds.

example

rpc.call('alert', { message: `Hello, world!`, duration: 3000 }, false);

actions

Display actions on the screen. You can set a listener to get called when an actions is pressed.

  • actions (Array) array of actions to display, in order. Each actions has:
    • text (string) The label for the action button
    • shortcuts (Array<string>) keyboard shortcuts for the action using event.key values from here.

example

rpc.call('actions', { actions: [
  { text: 'Stop the launch! (q)', shortcuts: ['q'] },
  { text: 'Resume! (w)', shortcuts: ['w'] },
  { text: 'Start over! (e)', shortcuts: ['e'] },
]}, false);

listener

To react to actions being pressed, you expose a listener from within your game. This is usually done at the top level, when the game is first loaded. Your callback handler will be called whenever an action is fired. Even if you later change the actions displayed you do not need to expose a new action handler.

rpc.expose('action', async ({ action }) => {
  const { text } = action; // The action object is the same as what you passed in to rpc.call(actions)
  switch (text) {
    case 'Stop the launch! (q)':
      rpc.call('load', { video: 'video.mp4' }, false);
      break;
    case 'Start over! (e)':
      rpc.call('seek', { index: 0 }, false)
      break;
    default:
  }
});

save

Save an object of state for later retrieval with the state rpc call.

  • state (object) your game state.

example

rpc.call('save', { state: {
  choice: 1,
  deaths: 20,
  level: 2,
}}, false);

state

Get the current game state

example

rpc.call('state').then(state => {
  const { deaths } = state;
});

custom_alert

Show a custom alert in an iframe. You provide the css/html, but no javascript will run.

  • html (string) The html document for the iframe
  • height (string) The css value for height
  • width (string) The css value for width
  • duration (number) How long (ms) the alert should show between the transitions
  • txduration (number) How long (ms) the transition to fade in and out should take Note: total duration is (duration + txduration * 2) Returns:
  • A promise for when the alert has disappeared.

example

// notice that we do not pass in `, false` at the end.
// this tells the rpc we want a callback
rpc.call('custom_alert', {
  html: '<html><head><style>body{background-color:white;}</style></head><body><center><h1>HI!</h1></center></body></html>',
  height: '500px',
  width: '50%',
  duration: 2000,
  txduration: 200,
}).then(() => {
  // the alert is now gone, safe to set a new one.
});

If you don't care about when it disappears, pass in , false at the end

rpc.call('custom_alert', {
  html: '<html><head><style>body{background-color:white;}</style></head><body><center><h1>HI!</h1></center></body></html>',
  height: '500px',
  width: '50%',
  duration: 2000,
  txduration: 200,
}, false);

Patreon Integration

If you would like to enable content in your game only for patrons, you can add some code to your game to be given some information about the patreon user that has logged in.

If you want to see a demo you can use this demo game.

Add some data to your package.yml

patreon:
  campaign: 5935402 # this is my campaign id for the cyoa.club patreon.

You can find your campaign id by:

  • Go to https://www.patreon.com/creator-home
  • view page source
  • search for "campaign": (with the quotes and the colon)
  • and then grab the id from the page:
      "relationships": {
        "campaign": {
          "data": {
            "id": "5935402",
            "type": "campaign"
          },
          "links": {
            "related": "https://www.patreon.com/api/campaigns/5935402"
          }
        }
      }
    

Call the patreon method in your game

It will return an object with the following information

  • user a string of the userid
  • status a string for the user's current status (you'll probably want to look for "active_patron")
  • tiers an array of tiers that the user is entitled for.
rpc.call('patreon').then(({ user, status, tiers }) => {

});

The user may be logged in, but not be a patron. You will still get the user id. This is helpful during testing because the patreon owner never shows up as a patron :)

Discord Integration

If you would like to enable content in your game only for those on your discord server with roles, you can add some code to your game to be given some information about the discord user that has logged in.

If you want to see a demo you can use this demo game.

Add some data to your package.yml

# make sure to quote the number to make it a string
discord: '1234567898764' # your server id (they call it a guild id)

Call the patreon method in your game

rpc.call('discord').then(
  /**
   * @param {object} args
   * @param {string?} args.id
   * @param {[sting]} args.roles
   */
  ({ id, roles }) => {

  }
);

The user may be logged in, but not have a role on your server. You will still get the user id.

Advanced Game Mode

If you are finding that, as a developer, the existing helpers for prompts, actions, and custom dialogs are too cumbersome... check out advanced game mode!

Just put this in your package.yml file:

advanced: true

This will set the body of your game's script frame as an overlay on top of the video. You will now have full control over any user interaction... buttons, text, forms, etc. The only drawback to this is that action keybindings will no longer work (your game frame will capture any keypresses.

This is not the end of the world, because actions should be easy to implement from within your game frame. Here's a simple demo, and another.

To help you style your page, I've allowed for a list of css files to be loaded into your frame (sometimes writing css in a css file is easier than inserting it via script).

advanced: true
css:
  - style.css
  • Feature List Overview
  • Getting Started
  • API
    • load
    • pause
    • play
    • seek
    • when
    • prompt
    • alert
    • actions
    • save
    • state
    • custom_alert
  • Patreon Integration
    • Add some data to your package.yml
    • Call the patreon method in your game
  • Discord Integration
    • Add some data to your package.yml
    • Call the patreon method in your game
  • Advanced Game Mode
CYOA
Docs
What is it?API Reference
More
Blog
© cyoa.club 2023