Naming Items and Rooms - ThePix/QuestJS GitHub Wiki

All things in the game world have a "name" attribute that they are identified by. Each should be unique, contain only standard letters, numbers and underscores, and not start with a number. That might be all you need.

However, due to the peculiarities of the English language, and the necessity of allowing for other languages too, naming items can get complicated... Consider a COUNTABLE item, brick. We might want to refer to a brick, some bricks, five bricks...

Simple names

The first parameter for the create function sets the "name" attribute.

createRoom("dining_room", {
  desc:'An old-fashioned room.',
})

createItem("chair", FURNITURE({sit:true}), {
  loc:"dining_room",
  examine:"A wooden chair.",
})

createItem("Kyle", NPC(false), { 
  loc:"lounge",
  examine:"A grizzly bear.", 
}

Quest will give the dining room an alias, "dining room" by removing underscores. It will note that "Kyle" starts with a capital, so is a proper noun. It will ensure the item name in the side pane is capitalised for both Kyle and the chair.

So far so good.

Setters and Getters

When considering how we use attributes it can be useful to split up how we set them and how we get them. With names there are several attributes that can be set to tell Quest how to handle this object's name. We will later look at how to get its name.

Setters

There are some odd situations that arise with regards to naming. While Quest will make a good guess at what it should do, sometimes it will get it wrong, and in these cases it is helpful to know a bit more about the attributes involved.

alias

The most obvious is names with unusual characters - and even a hyphen counts as unusual here.

createItem("ninty_degree_angle", TAKEABLE(), { 
  loc:"lounge",
  alias:"90° angle",
  examine:"A right angle!", 
}

In this case the name in the side pane will be derived from the alias, rather than the name, however you can also set the "listAlias" attribute yourself - or have it set dynamically by giving the item its own getListAlias function. The only example of that is for countables, where the number in that location is noted:

  getListAlias:function(loc) {
    return sentenceCase(this.pluralAlias ? this.pluralAlias : this.listAlias + "s") + " (" + this.countAtLoc(loc) + ")"
  },

synonyms, regex and pattern

It is important in a parser game that we set up each item to maximise the chances of the parser matching the item to what the user typed. The alias is the starting point, but there are three further attributes we can use.

The "synonyms" attribute is a list of alternative names the parser can use to match against.

synonyms:['torch'],

The is equivalent to the "alt" attribute in Quest 5.

Synonyms should all be lower case, even for proper nouns.

The "regex" attribute is a regular expression allowing alternative names to be matched. A couple of examples are given. Note ^ and $ are used to mark the start and end. Use a vertical bar to separate options. The ? indicates the preceding character is optional, so in the second example, the "s" at the end is optional.

regex:/^(me|myself|player)$/,
regex:/^(shoe|boot)s?$/,

The parser will consider a match with the "regex" or the "alias" or a string in "synonyms" to be good. Otherwise a partial match with the "alias" or "synonyms" will be taken as a lesser match (i.e., having a lower score, so less likely to be picked).

You can also use "pattern" attribute instead of "regex". This should be a string, that will be used to generate the regex (so you cannot use both "pattern" and "regex").

pattern:'me|myself|player',
pattern:'shoe|boot',

Other attributes

The "pluralAlias" attribute is, by default, only used by COUNTABLE items, but could potentially also be used for others where there might be more than one and they are grouped together. It will be set by default, and Quest will take a guess at what it should be, but you might want to check (for example, it will guess the plural of platypus is platypi, when obviously it is platypodes). You only need to use it if your object has an unusual plural form and might be referred to in the plural. One way to check is to type directly into the developer console (press F12 to see). If your item is called "platypus, type: w.platypus.pluralAlias.

The "properName" attribute is a Boolean that should be set to true if this item has a proper name, to prevent "a" or "the" getting prepended. Quest will set this automatically depending on whether the alias (or name if not set) starts with a capital or not, but you can set yourself to override. You might need to set it if the alias changes too (but again, Quest will take a guess about that).

The "indefArticle" and "defArticle" attributes can be used to override how Quest adds "a" or "the" to an item respectively. By default, for the indefinite article, nothing is added if it is a "properName" or mass noun, "some" is added if it is a plural, "an" is added if it starts with a vowel and "a" otherwise. For the definite article, "the" is added unless it is a "properName".

This example will have Quest use "an hourglass" rather than "a hourglass".

createItem("hourglass", TAKEABLE(), {
  loc:'table',
  indefArticle:'an',
}

The "listAlias" is what Quest uses for the side pane, while "headingAlias" is used as the heading when the player enters a location.

It is worth noting how alias and headingAlias are used for locations. In generated text it will be the alias that is used, for example to describe the butler entering the room. This could be quite different to the title, as this example shows:

createRoom("greenhouse_catwalk_west", {
  alias:'west end of the greenhouse catwalk',
  properNoun:true,
  headingAlias:'The Greenhouse (West, On Catwalk)',

You can change how Quest generates the default room heading by setting settings.getDefaultRoomHeading. This example prepends "the", unless it is flagged as a proper noun, then converts to title case.

settings.getDefaultRoomHeading = function(item) { return titleCase(lang.addDefiniteArticle(item) + item.alias) }

I would strongly advise deciding on a format earlier in the game creation process and ensuring all your room names conform to it. It is a serious pain in the neck to go though fifty rooms and change each one.

The "owner"

You can give an item an owner attribute, set to the name of another item. Quest will prepend the possessive adjective ("your", "his", etc) if the owner is the player, or the the possessive form of the alias (eg "Mary's") if not the player. You can change the owner, and indeed who the player is, during play.

NOTE 1: Setting the owner to an item that has this item as the owner will cause a loop that will crash your game. I recommend only ever making NPCs and the player owners, and not setting any of them to have owners.

NOTE 2: You can only set an owner to be an item (NPC, etc.) that already exists; that is the owner must be created earlier in the file or in a previously loaded file, if you are setting the owner when the item is created.

Changing the Alias and Related

You should never change an object's "name" attribute.

The "alias" attribute should not be set directly during play, as there are things set from it in the background that the parser uses. You can, however, change it whenever you like using the "setAlias" function; this will ensure other attributes relating to the parser are set correctly.

This example gives the mysterious man a new alias once his name is known. You can add a second parameter, a dictionary, where you can also set "listAlias", "pluralAlias" and "properNoun", as required - Quest will guess values for these based on the new alias if you choose not to.

w.mysterious_man.setAlias("Kyle")

This table shows the attributes that can be set from the start or changed with "setAlias".

Attribute Default Comment
alias name Set separately to options
listAlias sentenceCase(alias) Used in the side pane
headingAlias sentenceCase(lang.addDefiniteArticle(this) + alias) Used for location headings
pluralAlias lang.getPlural(alias) Mostly used by COUNTABLE items
properNoun /^[A-Z]/.test(this.alias) Determines if the/a should be pre-pended

In addition, the old alias will be added to the list of synonyms.

The parser

When it first meets a new item, the parser will take the alias and break it into parts, and then assemble it into various combinations. This means that "ham and cheese sandwich" will be matched to "ham and cheese" or "cheese sandwich" or "ham sandwich". Combinations will stay in their original order and can be up to three parts.

It should be able to understand GET HAM AND CHEESE SANDWICH as a request to get the ham and then get the cheese sandwich if there is a ham here and a cheese sandwich, or to mean get the sandwich with ham and cheese in it, if that is present.

The "setAlias" function sets "parserOptionsSet" to false, which will trigger the parser to reset its various attributes (all of which start "parser"). You can have your own "parser.itemSetup" function if you want more control. This example, which would be in code.js, strips out non-alphanumeric characters from the name parts, because the game has an item called copy of "Antony and Cleopatra".

parser.itemSetup = function(item) {
  item.parserOptionsSet = true
  item.parserItemName = item.alias.toLowerCase().replace(/[^\w ]/g, '')
  item.parserItemNameParts = array.combos(item.parserItemName.split(' '))
  if (item.pattern) {
    if (!item.regex) item.regex = new RegExp("^(" + item.pattern + ")$") 
    if (!item.parserAltNames) item.parserAltNames = item.pattern.split('|')
  }

  let list = [item.alias.toLowerCase().replace(/[^\w ]/g, '')]
  if (item.parserAltNames) {
    for (const el of item.parserAltNames) {
      const s = el.toLowerCase().replace(/[^\w ]/g, '')
      list.push(s)
      list = list.concat(array.combos(s.split(' ')))
    }
    item.parserItemNameParts = [...new Set(list)]
    array.remove(item.parserItemNameParts, 'and')
  }
}

pronouns

This determines how this item is to be referred, and should be a dictionary, the keys of which are illustrated here:

  pronouns:{subjective:"it", objective:"it", possessive: "its", poss_adj: "its", reflexive:"itself"}

That said, the best way to do this is to use the built-in dictionaries - and some functions assume this is the case:

lang.pronouns.thirdperson (the default)
lang.pronouns.firstperson
lang.pronouns.secondperson (default for `PLAYER()`)
lang.pronouns.male (default for `NPC(false)`)
lang.pronouns.female (default for `NPC(true)`)
lang.pronouns.nonbinary (uses they/them/etc, but in the singular)
lang.pronouns.plural
lang.pronouns.massnoun

It can then be set like this:

pronouns:lang.pronouns.male

If item is set to be female, for instance, item.pronouns.subjective will be "she". You can use "massnoun" for underwear, and "plural" for pants, for example.

The "pronouns" attribute does not get saved when the user saves her game. If this attribute changes during play for an object, you will need to ensure it gets saved in some way and then is reset when the game is loaded. An example can be seen here.

You can also set "parserPronouns", which is used only when the user types IT or THEM. A example of when this is useful might be a pair of boots. The item is singular - just one pair - but the player might want to EXAMINE THEM. So we set "pronouns" to lang.pronouns.thirdperson (in fact this is the default, so can be left) and "parserPronouns" to lang.pronouns.plural.

createItem("boots", WEARABLE(), {
  loc:"tool_room",
  scenery:true,
  parserPronouns:lang.pronouns.plural,
  alias:"pair of boots",
  // ...
}

getDisplayName

You can give an item a "getDisplayName" function attribute, and then it can sort out what should be displayed for a given situation. The function will receive a dictionary with various options that you need to take account of, the most important being:

Name Possible values Comment
article DEFINITE, INDEFINITE or undefined Should this have "the", "a" or nothing prepended?
possessive true or false/undefined Should this have 's appended?
count an integer or undefined How many are there?

You can also use your own parameters. Here is an example for a book.

  getDisplayName:function(options) {
    return 'a ' + options.adj + ' tome' 
  },

This can then be used, for example with the text processor.

msg("You see {nm:item:a:false:adj}", {item:w.book, adj:'mighty'})
  ->You see a mighty tome

Name Modifiers

A name modifier is a brief word or phrase that flags the current state of an object. It might flag a hat as "worn" or a torch as "providing light". Templates do this for you in a lot of instances, but you might want to add you own. Just add a "nameModifierFunction" function that accepts a list for the parameter.

createItem("dynamite", TAKEABLE(), {
  loc:"shed",
  examine:"A stick of dynamite. Be very careful...",
  nameModifierFunction:function(list, options) {
    if (this.hasBeenLit) list.push('fuse burning')
  },
})

The function could also be used to remove modifiers.

You can modify the format of the modifiers by adding your own util.getNameModifiers function (say in code.js). This version puts them in square brackets, separated by a vertical bar - all the action takes place in the penultimate line.

util.getNameModifiers = function(item, options) {
  if (!options.modified) return ''
  const list = []
  for (let f of item.nameModifierFunctions) f(item, list)
  if (item.nameModifierFunction) item.nameModifierFunctions(list)
  if (list.length === 0) return ''
  return ' [' + list.join('|') + ']'
}

Getters

If you are referring to a specific item, you know what it is, so just type the name of the item in the text.

msg("You pick up the boots, and stow them in your pack.")

However, there are a lot of situations where your code could be used for any number of disparate items. If you create a new command, the user might try to use that command with the pickaxe, the boots or Kyle, and we really want the text to automatically handle that. Therefore, when you refer to a general object in text the user will see, you should either use the lang.getName function or a text processor directive, as this will allow Quest to decide exactly how to modify the name, based on all the attributes discussed above.

lang.getName

The simple way is to pass the item to lang.getName. This example also illustrates how to access the right pronoun.

msg("You pick up " + lang.getName(item) + ", and stow " + item.pronouns.objective + " in your pack.")

You can include a number of options in a dictionary as a second parameter:

  • article: Set to DEFINITE to prepend "the" or INDEFINITE to prepend "a" (if appropriate)
  • modified: Set to true to have any modifier added such as "(worn)" for clothing or the contents for a container
  • [itemname]_count: The count (of a COUNTABLE) to be prepended to the name
  • possessive: Gives the possessive form (appending 's, or just ' if ends in s) if true
  • ignorePossessive: If article is set and the item has an owner, the item's owner will, by default, be prepended instead, so "Lara's carrot" or "your hat". Set this to true to ignore that, and have the usual "the" or "a" prepended. If using the item link library, you can set it to "noLink" to have the name prepended, but without item links.
  • possAdj: Set to an NPC. If that is the owner, it will use "his/her" rather than the name.

This improved version will have "the" before the item name, but only if appropriate to that item.

msg("You pick up " + lang.getName(item, {article:DEFINITE}) + ", and stow " + item.pronouns.objective + " in your pack.")

Text processor

A better way - in my opinion - is to use the text processor directive. Again we add a set of options in a dictionary as a second parameter, but this time to the msg function.

msg("You pick up {nm:item:the}, and stow {ob:item} in your pack.", {item:item})

The options here are the items we want to refer to in the text, and there is only one in this instance. The item is referred to as "item" in the directives, and we need to tell Quest that this relates to the item.

We are using two directives. The first gives the name, and is flagged to include "the" if appropriate. The second gets the objective pronoun for the item. For a full list of directives, see here.

What if another character could be picking up the item? Let us suppose the character, either an NPC or the player, is stored in a variable called "actor"...

msg("{nv:char:pick:true} up {nm:item:the}, and stow {ob:item} in your pack.", {item:item, char:actor})

If you look in lang-en.js, you will see nearly all the responses are done like this.

When it comes to countables, this can get complicated, but that is because there are a lot of ways to use it, and language is complex..

You can use a parameter, [itemname]_count, to specify the number. If the article specifier is "count" in the directive then "one" is issued if there is only one, rather than "a".

msg("{nm:item:count:true}", {item:w.brick, brick_count:5})
  ->Five bricks
msg("{nm:item:a:true}", {item:w.brick, brick_count:5})
  ->Five bricks
msg("{nm:item:a}", {item:w.brick, item_count:5})
  ->five bricks
msg("{nm:item:a}", {item:w.brick, brick_count:1})
  ->a brick
msg("{nm:item:count}", {item:w.brick, brick_count:1})
  ->one brick

If the item is called "item" in the directive, then you can just use "count".

msg("{nm:item:a}", {item:w.brick, count:5})
  ->five bricks

You can also use a different attribute of the item. In this example, the directive is given "count_this", so it will look for that in the parameters.

w.book.specialCount = 4
msg("{nm:item:a} and {nm:item2:count:false:count_this}", {item:w.brick, count:5, item2:w.book, count_this:'specialCount'})
  ->five bricks and four books

msg("Lara looks at the {nm:item:false:false:count_this} thoughtfully.", {item:w.book, count_this:'specialCount'})
  ->Lara looks at the four books thoughtfully.

w.book.specialCount = 1
msg("Lara looks at the {nm:item:false:false:count_this} thoughtfully.", {item:w.book, count_this:'specialCount'})
  ->Lara looks at the book thoughtfully.