JavaScript - ThePix/QuestJS GitHub Wiki
This page was written for people used to ASL, the programming language for Quest 5, so has a lot of comparisons between that and JavaScript. If you are not familiar with ASL, it may not be so helpful.
JavaScript version
QuestJS uses JavaScript version ES6 (ECMAScript 2015), which was released in 2015, and became available in browsers not long after. Users with older browsers will not be able to play Quest 6 games, but I think such users will be very few, given the way browsers nag you to update so much! This table (from here] indicates when each browser included ES6.
Chrome 58 | Edge 14 | Firefox 54 | Safari 10 | Opera 55 |
---|---|---|---|---|
Jan 2017 | Aug 2016 | Mar 2017 | Jul 2016 | Aug 2018 |
JavaScript has some important differences if you are used to ASL, the coding language for Quest 5.
ASL is a somewhat cut down language, and as it runs on top of the Quest player, you do not have access to everything, so is a little limited. One of the biggest limitations is that you cannot pass parameters to a script or return a value (well, you can, using delegates, but the editor does not support it, so it is a bad idea).
JavaScript is a fully-fledge programming language, and as everything in QuestJS is written in JavaScript, you have access to everything. When you convert from ASL to JavaScript, you will tend to encounter the downside of JavaScript, because your existing code will not be designed to make use of the features only in JavaScript.
Basics
The basic structure of an ASL script will be the same in JavaScript; that is to say, the order of each line and what each line does. The arrangement of blocks and loops will be almost identical; if
and else
are used the same and curly braces, {
and }
are used to denote the start and end for both.
As with ASL, comments are denoted by two slashes, //
. You can have block quotes too; these start with /*
and end with */
and can extend over multiple lines.
The world, w
In ASL the global world of the code contains all the objects in the game. If there is an object called "table", you can just refer to it as table
.
In JavaScript the global world includes every built-in function, elements of the web page, etc. It is therefore necessary to have a sealed off section that is the game world. This is called "w". To access an object called "table", you have to go via w
, so must use w.table
or `w["table"] to get it.
Numbers
ASL has two types of numbers, int
, short for integer, i.e., a whole number, and double
, short for double-precision floating-point number, which is a number with a decimal part (and rarely if ever used in Quest). JavaScript is rather unusual for a programming language in just having one type, number
, which is equivalent to a double
, but can be used as an int
with no problem in most cases.
The issue to be aware of is division, as the result of dividing an int
by an int
is another int
in Quest 5, so 3 divided by 2 is 1. In JavaScript it is 1.5. Arguably the ASL result is the surprise, but just be aware it is different.
To convert back to a whole number used one of these:
Math.round(5.5) // round to the nearest whole number, or the greater number if 0.5
Math.round(-5.5) // so these would be 6 and -5
Math.floor(5.5) // round to lower whole number
Math.floor(-5.5) // so these would be 5 and -6
Math.ceiling(5.5) // round to greater whole number
Math.ceiling(-5.5) // so these would be 6 and -5
Declare variables
In JavaScript you have to declare a variable before you use. If it is going to change, use let
, if it is not going to change, use const
.
const height = obj.getHeight() // height cannot change
let n // n can change
n = height + 4 // so now n changes
const list = [] // list cannot change...
list.push(n) // but its contents can
If you are unsure if a variable will change, use let
. There is also an older keyword, var
, which is much like let
, but has some odd behaviour and no advantages, so I suggest avoiding it, though you will see it a lot in on-line examples elsewhere.
[In fact, you do not need to declare them if your file is not in "strict" mode, but you will have less bugs if you use "strict" mode, so do declare your variables.]
End a line with a semi-colon... or not
I started putting a semi-colon at the end of each line, but they are optional, and I realised there is no point. It just adds clutter to your code.
If you have two statements on the same line, then you do need a semi-colon to separate them, but really this is to be avoided anyway (except in a for
loop, as we will see later).
You will find a lot of web pages by people insisting semi-colons should be used. There are some really weird uses of JavaScript that people cite (eg here, but the solution is removing a line break; adding a semi-colon does not solve the issue. The only time there seems to be an issue is with statements start with (
or [
, so be aware of that, and recognise that you are very unlikely to ever do that - I never have.
Testing equality and inequality
If you want to compare two variables to see if they are equal, in ASL you use an equals sign. For not equal, use less than then greater than:
if (height = 12) {
if (height <> 12) {
Most modern programming languages use two equals signs to differentiate from when you are assigning a value, and an exclamation mark and equals sign for "not equals". JavaScript is the same, but will first try to "coerce" the types, which can give some unexpected behaviour. It is safer to use three equals signs:
if (height === 12) {
if (height !== 12) {
For Boolean operations:
ASL JS
and &&
or ||
not !
JavaScript considers several value to be false, besides actual false
. That is to say, the if
statement below will fail if n
evaluates to any of these.
if (n) msg("This is true")
These so-called "falsy" values are:
false
null
undefined
0
'' // i.e., a string of length zero
NaN
A variable that has not been given a value is undefined
, as is any attribute that has not been set or even does not exist (in effect). A mathematical expression that used a value that was not a number will result in NaN
.
If you want to know if a variable exists, as opposed to whether it has been assigned a value, you cannot compare if to undefined
as JS will complain that it does not exist. Instead, test if its type is "undefined".
if (typeof myVariable === "undefined") msg("That does not exist!")
Strings
As in ASL, a string can be denoted by double quotes, but you can also use singles - as long as they are the same at both ends. This allows you to include quotes in your string.
s = "Strings with double quotes are cool, aren't they?"
s = '"No!" she said emphatically.'
Get and Has functions
ASL has a bunch of functions for checking if an object has a certain attribute or to get its value.
if (HasString(table, "alias")) {
msg(GetString(table, "alias"))
}
In JavaScript you can just access it directly. If you are not worried about what type the attribute is, you just want to know if it exists:
if (w.table.alias) {
msg(w.table.alias)
}
That said, if the attribute "alias" could be zero or false, be aware that JavaScript will treat these as not existing. If that is possible, and you want to know if the attribute exists, check against whether it is undefined
.
if (w.table.alias !== undefined) {
msg(w.table.alias)
}
If you need to ensure the type of the attribute, use typeof
:
if (typeof w.table.alias === 'string') {
msg(w.table.alias)
}
Note that you are comparing to a string, which could be "string", "number", "object" (which includes dictionary and array) or "boolean". If you need to test if it is an array (aka list), do this:
if (Array.isarray(w.table.listOfStuff)) {
msg('It is a list!')
}
In ASL you would probably not use GetString as you can use the .
operator there too (the function exists for the visual editor), however it is common to use GetBoolean as that handles the case where the attribute does not exist.
if (GetBoolean(table, "smashed")) {
msg(GetString(table, "alias"))
}
Not a problem in JavaScript, as a missing attribute is the same as one set to false inside an if
statement.
if (w.table.smashed)) {
msg(w.table.alias))
}
However, if you need to test if the attribute has been set to false, rather than not existing, you need to do this:
if (w.table.smashed !== false)) {
msg(w.table.alias))
}
Dictionaries and lists
Lists are usually called arrays, but I will stick to the Quest 5 terminology here (in some languages they are quite different things).
Dictionaries and lists are built in to JavaScript, making them easier to use, but somewhat different to ASL. Here is the ASL to create a list and a dictionary, in each case adding three items, then printing the first.
myList = NewStringList()
list add(mylist, "one")
list add(mylist, "two")
list add(mylist, "three")
msg(StringListItem(mylist, 0)
myDictionary = NewStringDictionary()
dictionary add(myDictionary, "first", "one")
dictionary add(myDictionary, "second", "two")
dictionary add(myDictionary, "third", "three")
msg(StringDictionaryItem(myDictionary, "first")
In JavaScript:
const myList = []
mylist.push("one")
mylist.push("two")
mylist.push("three")
msg(mylist[0])
const myDictionary = {}
myDictionary["first"] = "one"
myDictionary["second"] = "two"
myDictionary["third"] = "three"
msg(myDictionary["first"])
Or even quicker:
const myList = ["one", "two", "three"]
msg(mylist[0])
const myDictionary = {
first:"one",
second:"two",
third:"three",
}
msg(myDictionary.first)
JavaScript dictionaries are very useful, and pervade QuestJS extensively, so it is important to understand them.
Note that while Quest 5 dictionaries and lists could be set up to hold only objects, or only scripts, etc., that is not relevant in JavaScript.
Loops
JavaScript while
loops are just like ASL.
The for
looks are a little different. Here is the ASL version; i
is the variable inside the loop. It will start with a value of 3, go up to a value of 7, in steps of 2 (the last parameter defaults to 1 if missing).
for (i, 3, 7, 2) {
msg(i)
}
In JavaScript it looks like this (the one time you should have multiple statements on the same line), which is more complicated though arguably clearer, but more flexible:
for (let i = 3; i <= 7; i += 2) {
msg(i)
}
The for
statement has three parts:
let i = 3 // Initialisation: declare a variable i and set to 3
i <= 7 // Condition: continue while i is 7 or less
i += 2 // Step: after each iteration add 2
ASL has foreach
for iterating over a list or dictionary.
foreach (s, myList) {
msg(s)
}
foreach (key, myDictionary) {
msg(StringDictionaryItem(myDictionary, key))
}
JavaScript uses for
, but in a different format. Note that you have to use of
for a list and in
for a dictionary.
for (let s of myList) {
msg(s)
}
for (let key in myDictionary) {
msg(myDictionary[key])
}
Assignment operators
This is a cool feature in JavaScript that ASL does not have. If you are converting code, you can ignore it, but it is great when writing new code.
There are often times when you do arithmetic on two values and assign the result to one of them. Let us say the player buys an object, and you need to determine how much money the player now has. In ASL (and JavaScript):
player.money = player.money - item.price
In JavaScript you can also do this:
player.money -= item.price
You can also do +=, /= and *=.
Increment and decrement
Another short cut, when adding or subtracting 1. In ASL:
player.counter = player.counter - 1
In JavaScript:
player.counter--
You can use ++
to increment.
switch
statement
The In ASL:
switch (s) {
case ("cat") {
msg("It's a cat!")
}
case ("dog") {
msg("It's a dog!")
}
default {
msg("No idea")
}
In JavaScript, less curly braces, but more colons and the word break
after each script:
switch (s) {
case ("cat"):
msg("It's a cat!")
break
case ("dog"):
msg("It's a dog!")
break
default:
msg("No idea")
}
Conditional operator
This is a useful feature for setting a variable to one of two values based on a Boolean. I have a feeling it works in Quest 5 too, but is undocumented, so rarely used.
This is the syntax:
condition ? exprIfTrue : exprIfFalse
Here is an example. The value of reply
is being set, based on the value of char.happy
. If char.happy
is true
then the first string is used, if it is false
the second is used.
let reply = char.happy ? 'Of course I'll do that!' : 'No way!'
It is equivalent to this, but can be done in one step.
let reply
if (char.happy) {
reply = 'Of course I'll do that!'
}
else {
reply = 'No way!'
}
It is possible to nest conditional statements in each other to create some really complex code, but I would really advise against that. It is best used where the line will not be too long.
More here.
Calling scripts
ASL has some special commands to run scripts, do
for a script attached to an object, and invoke
for any.
do(myObject, "specialscript")
In JavaScript that is not necessary, you just call the function.
myObject.specialscript()
This makes it very easy to pass parameters to a script - in ASL you need to put them into a dictionary first. It also means you can get a return value from a script, something that requires delegates in ASL.
const result = myObject.specialscript(4, player)
Note that JavaScript is very relaxed about the parameters passed to a script. If you send more than the script is expecting, it will just discard the excess ones. If you send too few it will pad them out with the special undefined
value. This is both a blessing and a curse...
this
Objects and In ASL objects in the language are also objects in the game. Confusingly there is also a concept of objects that pervades most modern programming languages. The idea is that the language models the real world, and it is convenient therefore to capture that by associating data and functions with the thing they belong to. It is not actually that far from the ASL idea, you just need to be away that objects are more general than just the items in the game world.
Just to muddy the waters a bit, JavaScript is a bit odd because objects and dictionaries are the same thing. And we have already discussed dictionaries so you know all about them already!
ASL has a special word this
that can be used to refer to the object the script belongs to. JavaScript is just the same. An issue to be aware of is that if you have a script in a dictionary that is attached to an object, this
will refer to the dictionary, which may not be what you expect.
Other Quest 5 script commands
The picture
, wait
and msg
functions are the same in JavaScript, though they can be used with additional parameters in QuestJS, as discussed elsewhere.
Use clearScreen
instead of the ClearScreen
function. Instead of error("message")
do errormsg("message"). Instead of
finishdo
io.finish()`.
Instead of set
, you can just set attributes directly.
set(myObject, "size" 7)
att = "count"
set(myObject, att, 14)
In QuestJS.
w.myObject.size = 7
const att = "count"
w.myObject[att] = 14
const objName = 'table'
w[objName][att] = 14
The firsttime
function requires a unique id, as there is no (obvious) way to otherwise tell one call from another. You are probably best avoiding this altogether.
Because of the way QuestjS saves a game, creating and destroying things during game play is problematic. Therefore these commands have no simple alternative: create
, create exit
, create timer
, create turnscript
, destroy
. You should clone objects rather than creating them during play.
There is no equivalent to on ready
(should only be used by the built-in libraries), request
(already deprecated in Quest 5), 'rundelegate' (not required) and start transaction
(not required). I do not think there is a need for them.
Functions
A lot of functions can be quickly swapped for the JavaScript version, which does the same thing.
For others the change requires a bit of re-ordering. As JavaScript is object-orientated, several functions are part of the object, rather than floating free. The means the name of the function goes after the object it is associated with, separated by a dot, so for example Split(mystring, "!")
becomes mystring.split("!")
.
Many of the output functions are best done another way. If you want to change default values, that should be done in style.css. For a temporary change to the style of the writing, use the text processor.
Functions for variables
ASL | QuestJS |
---|---|
ToInt(string) | parseInt(string) |
TypeOf (object, string) | typeof object[string] |
TypeOf (value) | typeof value |
User Interface Functions
ASL | QuestJS |
---|---|
ClearScreen() | clearScreen() |
DisplayList(list, flag) | msgList(list, flag) |
msg(string) | msg(string) |
picture(string) | picture(string) |
List Functions
ASL | QuestJS |
---|---|
list add(list, value) | list.push(value) |
list remove(list, value) | arrayRemove(list, value) |
FilterByAttribute(list, key, value) | list.filter(el => el[key] === value) |
FilterByNotAttribute(list, key, value) | list.filter(el => el[key] !== value) |
IndexOf(list, value) | list.indexOf(value) |
list = ListCombine(list1, list2) | list = list1.concat(list2) |
ListContains(list, value) | list.includes(value) |
ListCompact | [...new Set(array)] |
ListCount(list) | list.length |
ListExclude(list1, list2) | arraySubtract(list1, list2) |
ListItem(list, n) | list[n] |
list = NewList() | const list = [] |
list = NewObjectList() | const list = [] |
list = NewStringList() | const list = [] |
list = ObjectListCompact() | [...new Set(array)] |
ObjectListItem(list, n) | list[n] |
StringListItem(list, n) | list[n] |
StringListSort(list) | list.sort() |
Dictionary Functions
ASL | QuestJS |
---|---|
dictionary add | dictionary[key] = value |
dictionary remove | delete dictionary[key] |
DictionaryAdd(dictionary, key, value) | dictionary[key] = value |
flag = DictionaryContains(dictionary, key) | flag = dictionary.includes(key) |
n = DictionaryCount(dictionary) | n = Object.keys(dictionary).length |
value = DictionaryItem(dictionary, key) | value = dictionary[key] |
DictionaryRemove(dictionary, key) | delete dictionary[key] |
dictionary = NewDictionary() | const dictionary = {} |
dictionary = NewObjectDictionary(dictionary, key, value) | const dictionary = {} |
dictionary = NewScriptDictionary(dictionary, key, value) | const dictionary = {} |
dictionary = NewStringDictionary(dictionary, key, value) | const dictionary = {} |
value = ObjectDictionaryItem(dictionary, key) | value = dictionary[key] |
dictionary = QuickParams(key1, value1) | const dictionary = {key1:value1} |
value = ScriptDictionaryItem(dictionary, key) | value = dictionary[key] |
value = StringDictionaryItem(dictionary, key) | value = dictionary[key] |
String Functions
ASL | QuestJS |
---|---|
CapFirst(string) | sentenceCase(string) |
Conjugate(obj, string) | lang.conjugate(obj, string) |
DisplayMoney(n) | displayMoney(n) |
DisplayNumber(n) | displayNumber(n) |
EndsWith(string, str2) | string.endsWith(str2) |
FormatList(list, str1, str2, str3) | formatList(list, {sep:str1, lastJoiner:str2, nothing:str3}) |
Instr(string, str2) | string.indexOf(str2) |
InstrRev(string, str2) | string.lastIndexOf(str2) |
IsNumeric(string) | !Number.isNaN(string) |
IsRegexMatch(string, regex) | string.match(regex) |
Join(list, str2) | list.join(str2) |
LCase(string) | string.toLowerCase() |
LengthOf(string) | string.length |
Mid(string, n1) | string.substr(n1) |
Mid(string, n1, n2) | string.substr(n1, n2) |
PadString(string, str2) | string.padString(str2) |
ProcessText(string) | processText(string) |
Replace(string, str1, str2) | replaceAll(string, str1, str2) |
Spaces(n) | spaces(n) |
Split(string, sep) | string.split(sep) |
StartsWith(string, str2) | string.startsWith(str2) |
ToRoman(string) | toRoman(string) |
ToWords(string) | toWords(string) |
Trim(string) | string.trim() |
UCase(string) | string.toUpperCase() |
WriteVerb | lang.nounVerb |
Random Functions
ASL | QuestJS |
---|---|
DiceRoll | random.dice |
GetRandomDouble | |
GetRandomInt | random.int |
PickOneChild | |
PickOneChildOfType | |
PickOneExit | |
PickOneObject | random.fromArray |
PickOneString | random.fromArray |
PickOneUnlockedExit | |
RandomChance | random.chance |
Maths functions
All the mathematical functions are the same, but require "Math." prepended and start with a lower case character, so "Log" becomes "Math.log".