Creating Commands - ThePix/QuestJS GitHub Wiki

There are two parts to the command process.

First there is finding the specific command that best matches what the player typed. Most of the attributes of a command revolve around this.

Then there is performing the action for the command, which uses primarily the "script" attribute.

However... before we get to that, it is worth checking to see if the command already exists. To do that, play your game, and try typing the command. If it says "I don't even know where to begin with that." then the command does not exist yet; anything else and it does.

If it does, you should either modify the existing command or [disable it|https://github.com/ThePix/QuestJS/wiki/More-on-commands#disabling-a-command] and create a new one. If you just want to change the effect of the command, i.e., the script attribute, you should modify the existing command. If your changes are more involved than that, it is probably better to disable it and start again.

Command Selection

Let us be honest here, command patterns are not as easy as they are in Quest 5. Part of the problem comes down to limitations of JavaScript, and in particular that it does not support named capture groups (in fact this feature is being introduced, and in ten years will not be a problem, but we are limited to features people have in their browsers right now). It is also difficult to convert a pattern to a regex if there are alternative words (eg "take #object#;get #object#"). That means we need to tell Quest what to do with each bit.

However, we can turn that to our advantage, and tell Quest the context at the same time, giving the parser a much better chance at getting ambiguous commands right.

Suppose we have a room with an NPC called Mary and a map. The user can type:

M,GET M

... and Quest will understand that Mary is to pick up the map. In Quest 5, it would not know if the user wanted the map to get Mary, or Mary get Mary or whatever!

So how do we do commands now?

There are two attributes we are concerned with here, "regex" and "objects". The "regex" attribute is a regular expression, whilst "objects" is an array telling Quest what to expect there.

Let us suppose we want to handle putting an item in a container. We want to be able to handle:

PUT HAT IN TO BIG BOX

PLACE TEAPOT IN CHEST

Note that anything the user types is normalised first; that is to say any spaces at the start or end are removed, multiple spaces together are reduced to a single white space and all letters are converted to lower case.

The Regular Expression

Regular expressions are a powerful tool in most modern programming languages (and implemented almost the same across all languages, which is very unusual!), but are not straightforward. However, we only need a very superficial understanding, and will take it in easy steps.

There are four parts to the input text, where each part is either an object or not. Looking at the first one, it breaks up like this:

PUT        not object
HAT        object
IN TO      not object
BIG BOX    object

Most commands will alternate like that; in fact Quest will struggle to handle commands without a separator between objects (it is possible but beyond the scope of this page). So now we construct the regular expression. Regular expressions start and end with a forward slash, just as strings start and end with quote marks. We put a ^ at the start to indicate this has to match the start of the input text, and a $ at the end (remembering that Quest will normalise the text first to remove extraneous spaces and convert to lower text). Each of the above parts then goes into brackets, so we end up like this:

/^
(PUT)
(HAT)
(IN TO)
(BOX)
$/

For two of those - the first and third - we have specific text (PUT and IN TO), but we might want to allow the user to type a synonym. Each alternative needs to be separated by a vertical bar. Note that in the third group, each option starts "in", so "in" by itself must be at the end to ensure the others are tried first.

We also add a question mark and colon, which just says we do not care what is there, as long as it matches - what we care about is the text in the other bits.

/^
(?:put|place|drop)
(HAT)
(?:in to|into|in)
(BOX)
$/

The other two groups can be anything, so we just put in ".+" to say match to anything (which is represented by the dot) that is at least one character (indicated by the plus sign).

/^
(?:put|place|drop)
(.+)
(?:in to|into|in)
(.+)
$/

We then put that into one line (note where spaces have been added - where the user will type them):

/^(?:put|place|drop) (.+) (?:in to|into|in) (.+)$/

Checking Your Regular Expressions

If you are at all unsure of your regular expression, it is a good idea to check it. You can do that online here. Put your regular expression in the top (everything except the slashes at either end), and then type the commands that should - and should not - match it. It will tell you exactly what gets put in each capture group too.

The "objects" array

Now we need to do that array. We have flagged two groups to be discarded with the question mark and colon, and two groups we are interested in, which will contain HAT and BOX respectively - so the array needs two elements. Each element is a dictionary, i.e., a set of name/value pairs, just like the command itself.

[
  {},
  {},
]

We need to tell Quest where to look and whether multiple objects are allowed. This is done with attributes called "scope" and "multiple". The "multiple" just needs to be set to true if multiples are allowed, or false (or omitted) otherwise.

For the "scope" attribute, you need to tell Quest where to look first. Built in options are isHeld, isHeldNotWorn, isWorn, isPresent, isHere, isReachable and isVisible. Note that these are functions of the parser object, so need to be prefixed "parser.":

[
  {scope:parser.isHeld, multiple:true},
  {scope:parser.isPresent, attName:"container"},
]

You can add your own scope testing functions very easily. Here is an example that tests if the item is in a location called "spellbook".

parser.isLearntSpell = function(item) {
  return (item.loc == "spellbook")
}

NOTE: This must be before the command in your file (or in an earlier file). If it is not you will see "WARNING: No scope (or scope not found) in command GovernWorld" in the console, as parser.isLearntSpell will be undefined when the scope is set. However, it cannot go in settings.js, as parser will not exists yet.

We are telling Quest that "isLearntSpell" is a function, which takes a parameter "item". The function returns true if item.loc, the name of the location of the item, is equal to "spellbook". Note that when testing if two things are equal you need to use three equals signs.

Other options

In the example above, you will see that the second entry has an "attName". This is a hint to the parser about which object the player is probably referring. The parser will guess it will be an object with the attribute. In this example, an object with a "container" attribute.

If you do not specify an "attName", Quest will use the name of the command, which is generally good enough. If you recall the CHARGE command from the tutorial, that would prioritise objects with a "charge" attribute - i.e., the torch - without us having to do anything.

There are various options for an object that determine how matching is to be done.

  • attName: Objects with this attribute are prioritised..
  • items: An array of names of items to be prioritised.
  • scope: Objects in this scope are prioritised.
  • multiple: Allow multiple objects, eg GET ALL (this is pretty easy to implement, so worthwhile considering).
  • extendedScope: If set to true Quest will search the entire world to match objects, rather than the usual places (essentially the player and location)

How "attName" and "items" is used is defined in the "parser.scoreObjectMatch", and could be customised for further control, including adding your own parameters.

Further options for the "objects" array

There are two types of entries for the "objects" array; special and object. Here is an example for handling ASK MARY ABOUT THE MURDER that uses special:

    objects:[
      {special:'ignore'},
      {scope:parser.isHere},
      {special:'ignore'},
      {special:'text'},
    ]

The special option tells the parser to handle this bit differently. For "ignore", it just ignores it (and generally that is better done in the regular expression as before). For "text", it passes the text to the command unaltered. There is also a "fluid" option that checks the text matches a string in the settings.fluids array, and if so passes the text. You can add your own too, see here.

Command scripts

I am using the Quest terminology of "scripts", that is not so common in JavaScript - despite the name!

If a command is selected as matching the user's text, its "script" attribute will be run. The default "script" attribute will hand responsibility for handling the command to the item (similar to how verbs work in Quest 5).

Use an attribute of the items

The simplest way to handle a command, then, is to use an attribute of the items (see also the tutorial). A simple example would be the EXAMINE command; you can set this to use the "examine" attribute of an item.

Note that the parser will give higher priority to command-item combinations where the item has the correct attribute.

The value of this attribute can be a string or function, and can vary from item to item, even for the same command.

You can specify which attribute the command should use with the "attName" attribute of the command; by default it will use the name of the command, in lower-case (in fact, it uses verbify to convert the command name to attribute name). If there are any non-alphanumeric characters in the name, you should specify an attribute that is only alphanumeric.

Here is the "Look at" command. The "attName" attribute is set to "examine", so if the player tries to examine an object, the object's "examine" attribute will be used (we cannot use "look at" for the attribute name as it has a space in it).

  new Cmd('Look at', {
    regex:/^(?:x|look at|examine) (.+)$/,
    attName:'examine',
    objects:[
      {scope:parser.isPresent}
    ],
  }),

If the command was called "Examine", we could omit the "attName" attribute:

  new Cmd('Examine', {
    regex:/^(?:x|look at|examine) (.+)$/,
    objects:[
      {scope:parser.isPresent}
    ],
  }),

The verb attribute

Your items can then be set up like this:

createItem("boots", WEARABLE(), {
  loc:"lounge",
  pronouns:PRONOUNS.plural, 
  examine:"Some old boots.",
})
  
createItem("knife", TAKEABLE(), { 
  loc:"me", 
  sharp:false,
  examine:function() {
    if (this.sharp) {
      msg("A really sharp knife.");
    }
    else {
      msg("A blunt knife.");
    }
  },
})

In the first example, "examine" is a simple string. It will be given parameters so you can use text processor directives described here.

In the second it is a function. This gives you more control; you might want additional checks and you will probably want the world to change. The function will get sent a dictionary containing "char", the character doing the command, and "item", the object involved. It should return true or false if successful or not - or more specifically whether a turn passes - though true is taken as the default.

This example uses the "char" entry to see who is reading the book.

  read:function(options) {
    if (options.char === w.Lara) {
      msg ("It is not in a language {pv:char:understand}.", options)
      return false
    }
    msg ("'Okay.' Lara spends a few minutes reading the book.")
    return true
  },

There is a short cut that allows you to print a message and return false in one go, as this is very common in command attributes.

  read:function(options) {
    if (options.char === w.Lara) return falsemsg ("It is not in a language {pv:char:understand}.", options)

    msg ("'Okay.' Lara spends a few minutes reading the book.")
    return true
  },

Adding a default

What happens if an item has no attribute? You need to include a default to handle it, called "defmsg". This should be a string but you can assume it will get sent to the text process with the usual parameters, so "char" for whoever is doing it, "item" for the item.

  new Cmd('Take', {
    regex:/^(?:take|get|pick up) (.+)$/,
    objects:[
      {scope:parser.isHere, multiple:true},
    ],
    defmsg:"{nv:chat:can't:true} take {nm:item:the}!";
    },
  }),

Allowing for multiple items

If the command allows multiple items (that is, the player can do EXAMINE ALL or LOOK AT BOOK AND FISH), Quest will go through each one and try it. If any one of them results in a success, this command will be considered successful, and turnscripts will run. To allow multiple items, you just need to flag that in the objects list as mentioned earlier. You may want to prefix your responses with "{multi}", which is a text processor directive, so needs to go inside the quotes - if this is part of a set of several items, Quest will prepend the item's name for you, to make clear what it is responding to. If you have fairly verbose responses, it is probable not necessary.

The two approaches are illustrated here:

msg("{multi}Taken!", options)
msg("{nv:char:take:true} the {nm:item:the}.", options)

Support for NPCs

We can easily allow the user to have the player tell an NPC to use the this command too, just add "npcCmd:true" to it, and Quest will do all the hard work (behind the scenes two new commands will be created to handle that).

For commands that involve a second item, like the PUT HAT IN BOX example earlier, you can access the second item from the "secondItem" value in the dictionary. This example checks the second item is a crocodile tooth.

  openwith:function(options) {
    const item = options.secondItem
    if (item !== w.crocodile_tooth) return falsemsg("Mandy wonders if she could open the grating with {nm:item:the}. She shakes her head - no, that will not work.", {item:item})
    return this.take(options)
  },

Custom script function

If you are doing something a bit odd you may need to handle it in the "script" attribute of the command itself.

The 'objects' list of lists

The script function takes a list of lists, and it is important to understand exactly what that will be. Let us go back to the earlier example, and suppose the user has typed PUT TEAPOT, HAT AND CUP IN CHEST. The command will be sent an array containing two elements, the first being what will put in the container, and the second being the container.

objects = [
  [w.teapot, w.hat, w.cup],
  [w.chest],
]

Suppose the user types KILL GOBLIN... There would still be a list of lists:

objects = [
  [w.goblin],
]

We will continue looking at the PUT/IN command. It is a good idea to create a function to do the work, as this will make it easy to add further commands instructing an NPC to do the command. With that in mind, the function needs to be sent the character who is doing it, plus the objects list discussed earlier.

function handlePutInContainer(char, objects) {
  // set up some useful variables
  let success = false
  const container = objects[1][0]
  const multiple = objects[0].length > 1 || parser.currentCommand.all

  // Check the container
  if (!container.container) return failedmsg("That's not a container!")
  if (container.closed) return failedmsg("It's closed!")

  // Go through each item
  for (const obj of objects[0]) {
    // Check each item; if it fails, continue to next item
    if (container.testRestrictions) {
      if (!container.testRestrictions(obj)) continue
    }
    if (!obj.isAtLoc(char.name)) {
      msg("You are not carrying " + lang.getName(item, {article:INDEFINITE}) + ".")
      continue
    }

    // If we get here, all is good, so go for it!
    obj.moveFromTo(char.name, container.name);
    msg("You put " + lang.getName(obj, {article:DEFINITE}) + " in " + lang.getName(container, {article:DEFINITE}) + ".")
    success = true
  }
  
  // If any item was successful, that is a success
  return success ? world.SUCCESS : world.FAILED
}

Note that to move the object we use its moveFromTo function. For most objects, this just sets the "loc" attribute to the destination, but some objects may work differently. For example, COUNTABLE objects need to add the count to the destination and deduct it from the source.

Note also that in the verb attributes we were returning true or false, or using "falsemsg". This script has to return world.SUCCESS or world.FAILED, or use failedmsg.

I would suggest using text processor directives for the messages; I think it is less typing and you get better messages. This version also allows for NPCs doing it. The loop would look like this:

  // Go through each item
  for (const obj of objects[0]) {
    // Check each item; if it fails, continue to next item
    if (container.testRestrictions) {
      if (!container.testRestrictions(obj)) continue
    }
    if (!obj.isAtLoc(char.name)) {
      msg("{nv:char:be:true} not carrying {nm:item:a}.", {char:char, item:obj})
      continue
    }

    // If we get here, all is good, so go for it!
    obj.moveFromTo(char.name, container.name)
    msg("{nv:char:put:true} {nm:item:the} in {nm:containter:the}", {char:char, item:obj, container:container})
    success = true
  }
}

The command, then, is just this:

  new Cmd('Put/in', {
    regex:/^(put|place|drop) (.+) (in to|into|in|on to|onto|on) (.+)$/,
    objects:[
      {ignore:true},
      {scope:parser.isHeld, multiple:true},
      {ignore:true},
      {scope:parser.isPresent, attName: "container"},
    ],
    script:function(objects) {
      return handlePutInContainer(player, objects);
    },
  }),

Instructing NPCs

Then we can add a further command to handle instructing an NPC. There are two forms, TELL NPC TO... and NPC,..., so we give it two regular expressions:

  new Cmd('NpcPutIn', {
    regexes:[
      /^(.+), ?(put|place|drop) (.+) (in to|into|in|on to|onto|on) (.+)$/,
      /^tell (.+) to (put|place|drop) (.+) (in to|into|in|on to|onto|on) (.+)$/,
    ],
    objects:[
      {scope:parser.isHere, attName:"npc"},
      {ignore:true},
      {scope:parser.isHeld, multiple:true},
      {ignore:true},
      {scope:parser.isPresent, attName: "container"},
    ],
    script:function(objects) {
      var npc = objects[0][0];
      npc.actedThisTurn = true;
      if (!npc.npc) return failedmsg(CMD_NOT_NPC(npc))

      objects.shift()
      return handlePutInContainer(npc, objects)
    },
  }),

Rules

A common part of a command script is checking if the command is allowed - for example, is the item the user wants to be picked up here? To make that easier, there are some built in rules that you can add to your command with the "rules" attribute.

The first five relate to the location of the item (only the first item if the command involves more than one):

cmdRules.isHeldNotWorn cmdRules.isWorn cmdRules.isHeld cmdRules.isHere cmdRules.isHereNotHeld

The other three check if the character doing the command is able to do so. The canTalkTo one also checks the item is an NPC, so can hear what is said.

cmdRules.canManipulate cmdRules.canTalkTo cmdRules.canPosture

Where the rule fails, an appropriate message is given.

You can make up your own rules too. The example below is for using a spray can. The rule is used to check the can is held. If it is, we can use the "spray" attribute of the target. The rule could also check if the can is empty, and if not, reduce the number of charges (the rules are only run once Quest has determined the target has the right attribute).

commands.push(new Cmd('Spray', {
  regex:/^(?:spray) (.+)$/,
  rules:[
    function(cmd, char, item, isMultiple) {
      if (w.spray_can.loc === char.name) return true
      return falsemsg(prefix(item, isMultiple) + "{nv:char:do:true} not have the spray can.", {char:char})
    }
  ],
  objects:[
    {scope:parser.isHere},
  ],
  defmsg:"You can't spray that!",
}))

Adding to your game

Once you have a command, you need to add it to the game system. Quest 6 looks for commands in an array called "commands", so it is simply a case of adding your command to that. JavaScript has two functions to do that; push adds an item to the end, and unshift adds it to the beginning, you can use either (I use unshift because at one time the order mattered, and now it is habit).

If you are overriding an exist command, you should give your command a "score" attribute, set to, say 10, so Quest gives it priority.

Here is an example where the command is created and added in one go. It overrides the built-in Credits command, so has a "score" attribute.

commands.unshift(new Cmd('HelpCredits', {
  regex:/^(?:credits?|about)$/,
  score:10,
  script:function() {
    metamsg("{b:Credits:}")
    metamsg("This was written by The Pixie, on a game system created by The Pixie.")
    return world.SUCCESS_NO_TURNSCRIPTS
  },
}))

A second example, this one uses an item, and calls a function, cmdDamage.

commands.unshift(new Cmd('Burn', {
  npcCmd:true,
  rules:[cmdRules.isHere],
  regex:/^(burn) (.+)$/,
  objects:[
    {ignore:true},
    {scope:parser.isPresent},
  ],
  useThisScriptForNpcs:true,
  script:function(objects) {
    const char = extractChar(this, objects)
    if (!char) return world.FAILED
    if (!haveFireSource(char)) {
      return failedmsg(nounVerb(char, "need", true) + " a source of fire to burn something.")
    }
    return cmdDamage("fire", char, objects[0], "#### will not burn.")
  },
}))

Note that the commands in "commands.js" look slightly different as they are added to the commands array when it is initialised.