Command Actions - ThePix/QuestJS GitHub Wiki

An approach to modifying commands is to build a list of exceptions. In this methodology, to create locations and items using just the standard attributes - essentially just exits and descriptions - and give items templates as appropriate. Separate to that, you create a list of command actions that can intercept a command in a specific situation to do something different. This is something like Specific Tasks in ADRIFT.

For example, one item could be a priceless vase. Dropping the item will break it, and we could handle that with a command action that is applied for the "Drop" command and the vase object.

Whether this is a good way to do it is debatable. My feeling is that you are probably better keeping the breaking of the vase with the vase item. However, this approach could be useful in an editor.

Also, it does have other uses that would be difficult to do other ways. Many of the examples below are for intercepting commands when the player is in limbo; when in that state the player cannot examine anything, and we can use a command action to handle that.

The command action

Command actions require a command name, a script and some parameters to determine when to use it.

Here is a simple example that just tests the player state. If player.inLimbo is true, the "test" function returns true and this action will be used, i.e., the "script" function will be run.

  new CmdAction({
    name:'Examine',
    test:function() { 
      return player.inLimbo
    },
    script:function() {
      msg("You cannot see, feel or hear anything...")
      return world.ABORT_FAILED
    },
  })

The function returns a special value that tells Quest the command failed, and not to bother with any further processing. Further processing means going through other items. If the player did EXAMINE HAT, BALL (and player.inLimbo is true), Quest will do the hat first, get to this action command, and then stop without bothering to do the ball.

Usually, your action will be specific to one item. This example only applies to the camera.

  new CmdAction({
    name:'Examine',
    test:function(item) {
      return item.name === 'camera' 
    },
    script:function(item, objects, multiple) {
      msg(multiple ? "Camera: Weird..." : "Weird...")
      return world.SUCCESS
    },
  })

Both "test" and "script" are passed the item, the other objects for the command (see here), and a flag to indicate if this is one of several items. That flag is used in this example to change the message.

In fact, testing for an item is so common there is a short cut. Note that it uses the name of the item, not the item itself.

  new CmdAction({
    name:'Examine',
    item:'camera',
    script:function(item, objects, multiple) {
      msg(multiple ? "Camera: Weird..." : "Weird...")
      return world.SUCCESS
    },
  })

You can also use "items", together with a list of item names, and the action will only apply to an item in that list; or "clone", and it will apply only to clones of the named item (and not the item itself).

You should only use one of "item", "items" and "clones", but it will often be useful to use one of them together with "test". In this example, any clone of the chrono-seed will be glowing if examined while in limbo.

  new CmdAction({
    name:'Examine',
    clone:'chrono_seed',
    test:function() { 
      return player.inLimbo
    },
    script:function(item, objects, multiple) {
      msg(multiple ? "Strange seed: It's... glowing." : "It's... glowing.")
      return world.SUCCESS
    },
  })

This example sets a flag on the item in the "script", while the "test" checks the flag is not set. This means it will only happen once.

  new CmdAction({
    name:'Examine',
    item:'magic_ring',
    test:function() { 
      return !magic_ring.inscriptionSeen
    },
    script:function(item, objects, multiple) {
      msg("You look at the curious ring, and for a moment think there is writing on the inside, but then it is gone. Probably just a trick of the light.")
      magic_ring.inscriptionSeen = true
      return world.SUCCESS
    },
  })

Order of command actions

You can have as many command actions as you like, so what happens if more than one applies? Very simply, Quest will use the first it finds, and only that one.

This means you need to put them in the file in a suitable order. I would suggest having all command actions in one file, and grouping them by command, and within each command grouping by item.

Action name

If you do not know the name of a command, you can look it up in lang/lang-en.js. The patterns for all the built-in commands are at the top of the file, like this:

    Examine:/^(?:examine|exam|ex|x|describe) (.+)$/,

The command name is the bit before the colon, in this case "Examine". You need to get the case right.

Commands without item

Some commands have no items.

SIT and STAND, for example, are just used on their own. This system works fine; obviously the "item", "items" and "clone" attributes make no sense here.

Some commands have parameters that are not items. The SAY command, for example, captures the text, rather than an item in the objects list. To get a command action to work with this, you will need to flag it as "ignoreItems".

Note that in this case world.ABORT_FAILED is used to prevent the normal command running as well, rather than to skip other objects.

  new CmdAction({
    name:'Say',
    ignoreItems:true,
    test:function(item, objects) {
      return player.inLimbo
    },
    script:function(objects) {
      const text = objects[1]
      msg("You try to say '" + text + "', but no words come out of your mouth.") 
      return world.ABORT_FAILED 
    },
  })

The SAY command also handles SHOUT, YELL, etc. and the first entry in the objects list is the verb, which is why objects[1]. This brings us to a couple of points to be aware of.

Some one-word commands use a generic command called "OneWord". Currently these are LISTEN, SMELL (and SNIFF), DANCE, SING and PRAY. These would need to be handled like SAY, remembering that you need to check that you have the right one.

  new CmdAction({
    name:'OneWord',
    ignoreItems:true,
    test:function(objects) {
      return player.inLimbo && objects[0] === 'pray'
    },
    script:function(objects) {
      msg("You pray, wondering if the gods can hear you when you are here.") 
      return world.ABORT_FAILED 
    },
  })

Note that the "test" and "script" functions do not use a item parameter, as that makes no sense in this context.

With Custom Commands

If you have your own commands, you can use command actions with them too. The only issue to be aware of is that if you command uses items, then the primary items must be first in the objects list. Usually that is the case any way, and you are good to go.

The "Clean" command is example where that is not the case, as it also captures the verb, in case the author wants to handle "DUST" differently to "RUB". The trick here is to flag the regex as reversed.

  new Cmd('Clean', {
    regexes:[{regex:/^(clean|rub|dust|polish|shine) (.+)$/, mod:{reverse:true}}],
    objects:[
      {scope:parser.isPresent},
      {special:'text'},
    ],
    defmsg:lang.cannot_clean,
  })