Controlling text with the text processor - ThePix/QuestJS GitHub Wiki

These directives change the content of the text depending on the state of the game world. Probably the most important use is to make descriptions dynamic. The dog might be yapping one time, or chasing his tail another time or sniffing the strange object. The warning light might be yellow or red or off. There might be a hole in the wall after the explosion.

Most of them will take the name of an object as a parameter, but you can also use these keywords:

keyword shortcut comment
player ?
currentLocation @
settings The values you set in the settings.js file
params * The dictionary of values passed to the text processor

Using parameters is discussed more elsewhere.

The directives are divided up into how many texts are included in the directive itself. For example, the if directive has two, one for when the condition is true, one for when if is false (though the latter is optional). Thus we have directives with no text, with either/or texts or multiple texts.

You need to be a little bit careful with spaces with these and other directives that are shown conditionally. You need to consider what spaces will be required in either situation. Usually it is best to have a space inside the text directive, at the start of the text, as indicated below.

The room is a mess.{once: That is a surprise!} Clothing is strewn all around.
                         ^

If the text is used, there will be a space before it and after it. If the text is not used, there will still be a single space between the two sentences. This example has the extra text as a fragment within a sentence. The comma needs to be inside the text directive.

The room is a mess{once:, which is a surprise}. Clothing is strewn all around.

You can add a further option will be used if the first string is not.

The room looks incredible!{once: It blows your mind: Been there, done that}.

No text options

In these cases, none of the text within the text processor director gets shown ever. The directive tells Quest to get the text from elsewhere.

nothing

This does nothing. It is available as it is sometimes useful to flag text.

A simple {!:this is ignored}sentence.

show

To show an attribute, use show. You then need the object, followed by the attribute name, all separated by colons.

You have {show:player:hitpoints} hit points.

You can actually omit the "show", and for compatibility with Quest 5, if "show" is omitted, you can use a dot to separate the object name and attribute name, however, some new features may not work so if you are creating a string from scratch use "show". Nevertheless, these will give the same result as the above:

You have {player:hitpoints} hit points.
You have {player.hitpoints} hit points.

If the attribute is a function, it should return a value; the function will be called, with the text processor parameters as a dictionary, and the result converted to a string and inserted into the text (if the returned value is an object you can add a further colon and an attribute name). This contrived example is from the unit tests.

  player.tpTest1 = function(params) { 
    return lang.toWords(2 * params.val) 
  }
  processText("Simple text: {show:player:tpTest1}", {val:8})

-> "Simple text: sixteen"

If the attribute does not exist or is set to null, undefined or false nothing will be shown.

showOrNot

To show an attribute, but also handle when it does not exist, use showOrNot. It works exactly like show except that it needs a extra parameter after the object.

You have {showOrNot:player:zero:hitpoints} hit points.

number and ordinal

Use these to display a number in words, rather than digits.

You have {number:player:health} hit point{ifMoreThan:player:hitpoints:0:s}.

Note that show can also be used to display parameter values, as discussed here. It will try to match an object in the parameters first, before looking for one in the game world.

You can add a noun, and the text processor will attempt to make the plural, if the number is not one. It is more sophisticated than just sticking as "s" on the end, but not infallible! The previous example could be done like this, with the same results.

You have {number:player:health:hit point}.

decimal

Use this to show a floating point number (i.e., not a whole number), to a specified number of places after the decimal.

Your armour rating is {decimal:armour:1}

hex and bin

Use these to show a number in either hexadecimal or binary. Why is that useful? No idea, but it was easy to do...

money

Use the money directive to display money, using the format set up in settings.js. This can be used with a number (it assume a whole number) or with the name of an object. If an object, the object's selling price is used if held by the player, or the buying price otherwise (see the MERCH template), if these are set. If not available, the price of the object is used. And if no price is set you will get an error message. If the object is the player, the player's "money" attribute is used.

You can use a dollar sign as a short cut for this directive.

The carrot is {money:carrot}
The carrot is {$:carrot}
You see {$:12}

dateTime

Use dateTime to display the current in-game date and time. The format is set in settings.

exits and objects and rooms

There are a number of directives that are designed to be used with the room template. This is how Quest decides the format for a room description. This is the default, an array of four strings, giving four paragraphs in the description.

  roomTemplate:[
    "#{cap:{hereName}}",
    "{terse:{hereDesc}}",
    "{objectsHere:You can see {objects} here.}",
    "{exitsHere:You can go {exits}.}",
  ],

The first line uses hereName to get the name of the current room (with "the" added if not flagged as a proper name). The cap directive makes it start with a capital letter. The # at the start of the line makes it into a heading (and is not actually a feature of the text processor).

The second line is the room description, using hereDesc, together with terse so the description is omitted in TERSE mode.

The third line lists the items here. The objectsHere directive means that if no objects are here this will not appear. The fourth line does the same for exits.

This offers a lot of flexibility. Here is an alternative version that puts everything in a single paragraph.

  roomTemplate:[
    "You are in {hereName}. {terse:{hereDesc}} {objectsHere:You can see {objects} here.} {exitsHere:You can go {exits}.}",
  ],

contents

Use "contents" to show the contents of a given item. As extra parameters, you can specify the list separator, the final separator and the text to use if it is empty.

You can see {contents:stasis_pod_drawer:,:and:nothing} stored in it.

Either/or text options

All of these check a condition; if true it uses the first text, if false it uses the second, though the second is optional in every case.

once and notOnce

Text with the "once" directive will only be seen once, the first time the text is displayed. Conversely, if "notOnce" is used, the text is only seen after the first time.

The room is a mess.{once: That is a surprise!} Clothing is strewn all around.

You can use notfirst as synonym, to ease conversion from Quest 5. You might also want to look at the various functions that fire when a room is entered, described here. For text that is just seen once, but could be in any of several descriptions, consider a custom directive, as shown here.

It is bad practice to put anything important in text that will only be seen once, as users can miss it, and then have no opportunity to find it again.

if and ifNot

You can also use the Quest 5 format for all if directives, to ease conversion.

These will test the value of an attribute. It needs the object name, then the attribute name.

If the attribute is a Boolean (and if it does not exist, in which case it will be taken to have the value false) it then needs the string to show if true, then, optionally, the string to show it false.

Player has that odd thing: {if:player:someOddAtt:yes:no}

If the attribute is an integer or string, it needs the value to test against, before the values to show.

Is the player exactly forty? {if:player:age:40:yes:no}

You can also use a Boolean in the params.

  msg("Simple text: {if:old:yes:no}", {old:player.age > 16})

The ifNot directive is the same, but works in reverse.

If the attribute is a function, the function will be called, with the text processor parameters, and the resultant value used for comparison (if the result is an object, its name will be used). This contrived example is from the unit tests.

  player.tpTest2 = function(params) { return 2 * params.val }
  "Simple text: {if:player:tpTest2:16:yes:no}", {val:8})

-> "Simple text: yes"

NOTE: When using with numbers and strings, you need to be certain these attributes exist. If you are thinking the attribute should be a number or string, but in fact it does not exist, Quest will assume if is a Boolean, and take the false value, which will almost certainly be wrong. In the above example, if there is no player.age attribute, Quest will output "Is the player exactly forty? yes". See next section!

ifIs and ifNotIs

Like "if" and "ifNot", but Quest will try to guess the type based on the content of the text directive, rather than the attribute on the object. This can be helpful if it might not exist. Looking at the player age example again:

Is the player exactly forty? {ifIs:player:age:40:yes:no}

In this case, Quest will see the parameter is 40, and will assume we are interested in a number. It will then look at player.age, if that is 40, we get "yes", otherwise we get "no", even if player.age does not exist.

For Boolean attributes, you need to specify its value, unlike with "if" and "ifNot":

Player has that odd thing: {if:player:someOddAtt:true:yes:no}

If you check if a Boolean attribute is true, then if the attribute has any other value, or does not exist, this will say "no", as you would expect. If you are checking for false, then any other value will say "no", so in this case no attribute will behave the same as the attribute being true.

ifExists and ifNotExists

Like "if" and "ifNot", but they test if the attribute exists on the given object, rather than testing what it is.

ifPlayer and ifNotPlayer

Like "if" and "ifNot", but they test if the given object is the player.

... {ifPlayer:char:That strikes you as odd. }...

ifLessThan, ifMoreThan, ifLessThanOrEqual, ifMoreThanOrEqual

These are similar to "if" and "ifNot", but can only be used with integer values.

Is the player forty or over? {ifMoreThan:player:age:39:yes:no}

See also the example in "number".

ifHere, ifNotHere, ifHeld, ifNotHeld

These are also similar to "if" and "ifNot", but depend on whether the named object is here (in the room) or held, i.e., isAtLoc returns true for player.loc or player.name respectively.

This is a messy room.{ifHere:puddle: There is a puddle of water on the floor.}

The Quest 5 format is also supported for "here".

ifAt, ifNotAt

Again similar, they depend on whether the named object is the current location.

a curious green stone.{ifAt:ritual_room: It is glowing!}

Multiple text options

These allow any number of texts in the directive.

random

The random directive will show one of the following options, picked at random:

There is a small dog here. {random:It is chasing its tail:It is yapping at nothing:It is looking at you strangely}.

As with Quest 5, The text processor is invoked when text is printed to screen and this means random text will be randomised every time it is printed. That is good if you want some random event to add interest, but not so good if the ball has a random colour each time the player looks at it. In the latter situation, you should invoke the text processor when the item is created, with the processText function. This example shows how you could set the examine attribute of a ball:

createItem("ball", TAKEABLE(), {
  loc:"dining_room",
  examine:processText("The ball is {random:blue:red:green}."),
}

select and cycle

You can use "select" to pick from a list in the directive itself - kind of like "random", but using an attribute to select a specific value. In this example, the "colour" attribute of the item is used to pick a value. In this case green will get displayed.

  w.Kyle.colour = 1
  msg("Kyle is {select:Kyle:colour:red:green:blue}.")

Note that it counts from zero!

To aid conversion from Quest 5, you can also do "Kyle is {select:Kyle.colour:red:green:blue}.".

Alternatively, you can select from a list. In this example, which is effectively the same as above, the object "Kyle" is given two attributes, one for the list and one for the value. Changing the value of Kyle.colour will cause the message to change when next printed.

  w.Kyle.colours = ['red', 'green', 'blue']
  w.Kyle.colour = 1
  msg("Kyle is {select:Kyle:colours:colour}.")

Note that both attributes must be on the same object. Also note that the order is reversed! Previously it was the state-tracking attribute, followed by the options, now it is the options (or the name of the attribute for them), followed by the state-tracking attribute. The reason for this is that it makes it easier for Quest to work out what is happening. It checks if the first bit is an array, and decided what to do from there.

This version is probably better for longer descriptions, and for where the same list is used in different places. This example shows how you can have an object's description change when its state changes.

createItem("robot" {
  loc:"lab",
  state:0,
  descs:[
    "A bunch of junk... but perhaps you can make something from it?",
    "It looks ugly, but it is a working robot.",
    "The robot's eyes are glowing red - never a good sign.",
    "A pile of junk that used to be a robot.",
  ],
  examine:"{select:robot:descs:state}",
}

There are actually four variations that you can use to determine how Quest deals with values out of range.

Name Effect
selectNone Nothing (same as the default)
selectWrap Repeated cycles through the list
selectStart Returns the first string
selectEnd Returns the last string

The "cycle" directive works exactly the same as "select", except that after each use the value is incremented. While "select" is excellent for making your descriptions match the game state, cycle is great for adding variety; you can use it in a room description for a location the player will see often, and quest will change the description by going through each possibility.

As with "select", there are four variations. This example uses "cycleWrap", so after it gets to the end of the list, it just repeats. Note that the first option has another text processor directive in it for more variation.

createRoom("street" {
  state:0,
  descs:[
    "A {random:red:black:blue} car is passing by.",
    "You hear a dog bark.",
    "It seems pretty quiet...",
  ],
  examine:"You are stood in the street outside your house. {cycleWrap:street:descs:state}",
}

Note that Quest will object if the attribute tracking the text to use ("state" in the example above is missing for "select", as it will assume this is a bug. It will not object for "cycle"; it will create the attribute and set it to zero, as this is usually what you meant to do anyway. The above example could therefore be done like this:

createRoom("street" {
  descs:[
    "A {random:red:black:blue} car is passing by.",
    "You hear a dog bark.",
    "It seems pretty quiet...",
  ],
  examine:"You are stood in the street outside your house. {cycleWrap:street:descs:state}",
}

You could even check the value of w.street.state elsewhere, but be aware that it will not exist until the text is used.

roomSet

You can create a set of rooms using the ROOM_SET template. This directive will then print text according to the order the rooms in the set are entered.

This is useful for locations that have some commonality, but the player can approach from multiple directions. Consider a corridor, which is set up as two locations, north and south. If the player enters the north end first, she will be amazed by the fancy ceiling. However, if she has already been down the south end of the corridor, she has already seen it, and it would be odd if she blown away by it a second time.

Here is how the description for the north end might be set up.

{roomSet:When you enter the corridor, you are blown away by the amazing ceiling:This part of the corridor is like the other:More of he same corridor}. It continues south, and there is a door north.