Implementation Notes - ThePix/QuestJS GitHub Wiki
Performance
There is a compromise between speed and convenience. Convenience here is how easy it is for authors to create games, and includes how simple it is to create a basic item, how easy it is to customise to do the bizarre and how easy it is to understand the author's code.
It would be faster to load all the JavaScript files at the same time, however, they would then run more-or-less in size order, with the smallest file going first. That will make it impossible for one file to reliably access something in another file - the most obvious example of that is using createItem
in your data files needs the file where createItem
is defined already loaded. Now there are ways around this - just delay when createItem
is called - but this adds extra complications to the data files and, rightly or wrongly, I chose not to do so.
The second issue is with layout reflow. The browser does a lot of work trying to decide a page layout, and if you then change the layout part way through, it has to start again. I am not sure how big an issue this is with QuestJS, but again I took the decision that convenience to authors was more important. Testing on my PC indicates this typically takes 50 ms which to my mind is perfectly reasonable.
With regards to commands, the slow bits are searching for the best fit command, which takes 15 to 20 ms on my PC, and running world.endTurn which takes a similar time. This means commands are being completed within 50 ms, which is fine.
Initialisation
Initialisation involves loading the right files in the right order. This is complicated by the fact that the files can be in different places. The game (i.e., those usually fond in the "game" folder, the ones specific to a particular game) files can be in a different directory to allow multiple games to be written at the same time. However, the editor also has its own ideas about where files are, the game files being created on-the-fly, while others are static files in a very different location. The index.html file therefore has a script
section that sets up some global variables that define where these things are, while the editor can create its own version with its own values.
The initialisation process is kicked off in index.html, which loads lib/_util.js, lib/_settings.js and then your game settings.js file. Once the last has loaded, settings.writeScript
is called.
The settings.writeScript
file loads all the CSS files, then sets the favicon, and then queues each script file, using settings.loadScript
. When a script file has been loaded, it is run, so items and locations are created at this point. As each is done, settings.scriptOnLoad
is called, which either starts the next script loading, or, if the queue is now empty, kick-starts the game, first calling world.init
then io.init
.
The world.init
function does some basic housekeeping, including initialising all the world objects, finding the player object and initialising all the commands.
The io.init
function sets up the user interface. It creates the side panes, adds event listeners to capture certain keypresses, loads sound files. Then it removes the game loading message, and displays the title. If the game has a starting dialog, that is now displayed, otherwise world.begin
is called (if there is a dialog, world.begin
will be called when that is closed).
The world.begin
function shows the introductory text, settings.intro
and runs setting.setup
, then calls world.enterRoom
which is the function that is called every time the player enters a room, so calls "afterEnter" and "afterFrstEnter".
A Turn
A turn is initiated by the player typing something and clicking enter.
The text is passed to the parser, which will attempt to match the text to a command. If it is successful, the command's "script" attribute is run. Details here. The parser calls world.endTurn
to kick off the end-of-turn housekeeping.
- Change listeners are handled
- Game turn and time are incremented*
- "endTurn" functions in objects are run*
- "endTurn" functions in modules are run*
- Change listeners are handled again*
- The "pause" state for any items is reset*
- Scope for items is updated by
world.update
* - Game state is saved (for UNDO)*
- The UI is updated
Items marked * occur only if the command returned world.SUCCESS
.
Data Structures
This could also be called namespaces; it is kind of the same thing in JavaScript.
Note that only w
will be saved (and so game
indirectly); nothing else gets saved.
w
The world is held in a dictionary called w
. This means you can access any item by prefixing its name with "w." and can access an item by a variable with square brackets.
// You can access the object called "hatstand" two ways:
name = "hatstand";
w.hatstand;
name = hatstand
w[name];
The advantage of using a dictionary is there is no chance of a name collision. The w
stands for "world", and is deliberately short.
game
Some transitory game values are stored in a dictionary called game
for convenience. It should be reset whenever the player changes rooms with game.update()
, and when the lighting changes and possibly other times too.
game.turnCount
game.elapsedTime
game.elapsedRealTime
game.dark
The game
object is in w
so string, integer and Boolean attributes will get saved.
commands
An array of all the commands in the game. Do not expect this to get saved.
cmdRules
Built-in rules for commands.
parser
Functions for the parser.
io
Hidden functions and variables for I/O.
tp
The text processor.
world
For the world model.
lang
Language specific settings.
test
For unit testing
No data files
A lot of IF systems, including Quest 5, have software files and data files as two distinct things. The data files contain all the information for a specific game, all the details for each item and location and probably more besides. The software loads it in and runs the game. Why not do that in Quest 6?
The problem arises with scripts. People will inevitably want to do something new, so absolutely need the ability to code or to write their own scripts, and that has to be in the data files. To then run code from a data file, you either need to run JavaScript, using eval
(or some variant), which has significant security issues, or you need some kind of interpreter in the software, which is the approach Quest 5 takes (and I guess other systems).
It seemed better all around to have the data in JavaScript too. Scripts can be added very easily because it is already code. You have the full power of JavaScript immediately available, with no effort from me. And the data is easier to write too, than say XML (which Quest 5 uses) or JSON.
No Named Capture Groups
Quest 5 uses named capture groups. JavaScript supports named capture groups. So why does Quest 6 not use them?
Simply because they are not universally implemented across all browser. This table indicates the version and date that various browsers introduced them:
Browser | version | date |
---|---|---|
Edge | 79 | 15/Jan/20 |
Firefox | 78 | 30/Jun/20 |
Chrome | 64 | 24/Jan/18 |
Safari | 11.1 | 29Mar/18 |
Opera | 51 | 7/Feb/18 |
Android browser | 81 | 21/Apr/20 |
Chrome for Android* | 87 | 17/Nov/20 |
Data from here
The first upload of Quest 6 to Github was 2/Dec/18. Edge and Chrome had named capture groups for eleven months by then, but it would be some time before Firefix would. And who knows how many users had updated their browser?
Even as I write this (08/Dec/20), only 91% of users have browsers that can use this feature, so even now I would be reluctant to use it. In two years time I expect the situation will be different, but I have to go with what it is right now.