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 videoindex- (number) time index, in seconds, to start playing at ex:123.456overlayidx- (number) time index to start at for the overlayloop- (boolean) if you want the video to loop when it reaches the endbeats- (string) name of beat file to load (matches declared file in package.)beatData- (array) of beat data. Don't use both beats and beatDatabarFile- (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.456overlayidx- (number) time index to start at for the overlaydelta- (number) time, in seconds, change by ex:-123.456(backwards) or2(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.456Returns:- 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 prompttitle- (string) Text for the title of the promptcontent- (string | 'Array<form elements>) The contnet of the promptchoices- (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 messageduration- (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 buttonshortcuts(Array<string>) keyboard shortcuts for the action usingevent.keyvalues 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 iframeheight(string) The css value for heightwidth(string) The css value for widthduration(number) How long (ms) the alert should show between the transitionstxduration(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
usera string of the useridstatusa string for the user's current status (you'll probably want to look for "active_patron")tiersan 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