Attributes for items - ThePix/QuestJS GitHub Wiki
This lists the attributes you might want to add or modify for items in your game. Several are functions; I am wanting to pass responsibility for decisions to the item, so, for example, the container can decide if it is blocking access to its contents (which by default depends on the "closed" attribute, but can be modified for your container).
Note 1: Attributes will only be saved if they are string, number, Boolean, string array or number array. Functions, other arrays and objects will not be saved.
Note 2: For how to display and track the value of attributes in the user interface, see here.
Names
There are a surprising number of attributes just to give an item a name! The only one you must provide is "name", and that is usually enough. However, using the others where required will make your game far more slick, as Quest will know how to properly refer to the object.
When you refer to a general object in code that will produce text the user will see you should be using the lang.getName
function - or better, use a text processor directive - as this will allow Quest to decide exactly how to modify the name.
For full details on attributes related to names, go here.
Location attributes
These are attributes involving in defining where the item is. Note that "loc" (short for location) is always a string, the name of the location, not the object that is the location (because of the way games are saved). To change a location, it is often best to use moveChar
for the player or an NPC, but either way this is discussed here). For items it is usually best to use the moveToFrom
method described below, though if you know for certain what the item is and that it has a "loc" attribute, and you do not want events getting triggered, it should be safe to set "loc".
loc
Used by the default "isAtLoc" to determine where the item is, so for most items you just set this. If you do not set it, or set it to undefined
, the item will be nowhere. This is often useful!
Note that if an item has a custom "isAtloc" or "isLocatedAt" (or uses a template that does, such as COUNTABLE items), this attribute may well be ignored.
Only use this attribute to determine an item's location when you are certain it will exist for that specific item. Commands, for example, are generally designed to handle any type of item, and in that situation you cannot rely on the "loc" attribute being used for the item, and should use "isAtLoc" instead.
isAtLoc(location_name, situation)
This is the definitive source for where an item is - this is how Quest ultimately decides where things are.
Behind the scenes, this uses two functions to decide, the first, "isLocatedAt", using the location and the second, "isApparentTo" using the situation, as discussed later.
If you want to know if something is here, use "isAtLoc". However, if you want to modify the behavior of an item, it is generally easier to give is a custom "isLocatedAt" or "isApparentTo".
The "isAtLoc" should be able to accept either the name of the location or the location object, and ideally will also check the location exists; "isLocatedAt" expects the name of the location.
This example of a custom "isAtLoc" is from the COMPONENT template.
isAtLoc:function(loc, situation) {
if (typeof loc !== "string") loc = loc.name
if (!w[loc]) errormsg("The location name `" + loc + "`, does not match anything in the game.")
if (situation !== world.PARSER && situation !== world.ALL) return false;
let cont = w[this.loc];
return cont.isAtLoc(loc);
},
isHere()
This is a useful shortcut for determining if the item is at the current location.
isHeld()
This is a useful shortcut for determining if the item is held (its location is the player). It will include items that are worn.
isHereOrHeld()
This is a useful shortcut for determining if the item is at the current location or is held (its location is the player).
isLocatedAt(location_name, situation)
Is this object at the given location? By default, this returns true
if the given location matches the "loc" attribute (which must be the name of the location, not the location object), but you can override this to give ultimate control on where an item is. If it always returns true
, an item will be everywhere. In this example, the cabinet is in two rooms (and note, has no "loc" attribute):
createItem("glass_cabinet", CONTAINER(true), {
examine:"A cabinet with a glass front.",
transparent:true,
isLocatedAt:function(loc) {
return (loc == "lounge" || loc == "dining_room")
}
})
For an item that is everywhere, check if the location is actually a location (so it does not appear in the player inventory or containers):
createItem("walls", {
examine:"They're walls, what are you expecting?",
regex:/^wall$/,
isLocatedAt:function(loc) { return w[loc].room },
});
isApparentTo(situation)
This is used by "isAtLoc" to decide if we want to hide the item for this situation. Why would we want to do that?
A common reason is because an item is flagged as scenery. We want the parser to be able to find the object here, but the list of items in the room.
In general, it is probably not that good an idea to have custom "isApparentTo" functions. If you have strange situations, either handle it in the "isLocatedAt" function if it is specific to an item, or use a custom function that gets used for everything, as this will ensure consistency across your game. However, it may be useful to know how it works, so we will look in depth.
The situation could have any of these values:
world.ALL
world.PARSER
world.SIDE_PANE
world.LOOK
world.INVENTORY
world.SIDE_PANE
world.PURCHASE
If the parser is asking if the item is there, situation will be world.PARSER
. If it is the LOOK command, or the equivalent, then situation will be world.LOOK
. A scenery object would return true
for the former, but not the latter. If the information has been requested by the inventories in the side pane, the value will be world.SIDE_PANE
. If the situation is world.ALL
, then the item should just be there (this is used to determine if a container holds the item or an NPC has it); world.INVENTORY is for when the player uses the INVENTORY command, and world.PURCHASE for when making a purchase.
You could add your own situations. They are actually integers behind the scenes, and to avoid any conflicts (and allow the base game to have additional options in later updates), just assign your custom situation to a high value in code.js, say over a hundred.
world.CAST_SPELL = 101
This simple example just returns false
for the room description if this item is flagged as scenery, and true
otherwise.
isApparentTo:function(situation) {
if (situation === world.LOOK && this.scenery) return false;
return true;
},
Note that it is good to flag an item as "scenery" even if you have a custom "isApparentTo" function that serves that purpose, as the parser will check that when the player does ALL; if flagged as scenery it will be excluded from the list.
Changing the default
If you want to change how things are done across the whole game, you can have a settings.defaultIsApparentTo
function in settings.js.
settings.defaultIsApparentTo = function(situation) {
if (situation === world.LOOK && this.scenery) return false
if (situation === world.SIDE_PANE && this.scenery) return false
return true
}
Note, however, that you can control whether scenery items are seen in the side pane with settings.showSceneryInSidePanes
, and I cannot think of any other cases where this might be useful.
countAtLoc(loc)
For a COUNTABLE item, returns the number at the given location. Otherwise returns 1 if the item is there, 0 otherwise.
moveToFrom(options, to, from)
Use this to move an item. This will ensure items that do not use the "loc" attribute get moved properly, and reactive functions get called.
NOTE: For characters, including the player, use moveChar
as described here.
The options parameter is a dictionary, with attributes: "char", the character doing it; and "item", the item being moved; and optionally "count", how many are being moved.
The "to" and "from" parameters are clearly used to indicate when the item is going to and from. Often these are quite general. For the TAKE command, the "to" is whichever character is doing it, the "from" is wherever the item is originally. To handle this situation, just use the strings "char" and "loc".
const options = {char:player, item:myItem}
myItem.moveToFrom(options, "char", "loc")
If "options" includes either "holder" or "container", the "loc" will indicate that location instead. This example could be when the item is given to another character.
const options = {char:player, item:myItem, holder:otherChar}
myItem.moveToFrom(options, "loc", "char")
You can also give a specific location, either by name or the object itself.
If you have an item with a custom "isAtLoc" or "isLocatedAt" you will need to give it a custom "moveToFrom" if the item can be moved. You should use util.setToFrom
to set "fromLoc" and "toLoc" in options following the rules above.
moveToFrom:function(options, toLoc, fromLoc) {
util.setToFrom(options, toLoc, fromLoc)
let count = options.count ? options.count : this.extractNumber()
if (!count) count = options.fromLoc === player.name ? 1 : this.countAtLoc(options.fromLoc)
if (count === 'infinity') count = 1
this.takeFrom(options.fromLoc, count)
this.giveTo(options.toLoc, count)
},
The "afterMove", "afterTakeOut" and "afterDropIn" functions use this same set of options, with "toLoc" and "fromLoc" set.
afterMove(options)
If an item has this function, it will be called after the item is moved.
Note that if this function uses moveToFrom
to subsequently move the item, you could end up in an endless loop! You will need to use "loc" and some care in that situation.
afterTakeOut(options)
, afterDropIn(options)
These are attributes of the location (or container or character), and will be triggered when an item goes from it or arrives at it - as long as moveToFrom
has been used. This means you can have a container or room respond when an item is put there or is taken away.
previousLoc
Stores the previous location for the player and NPCs (as long as they are moved using world.setRoom
). Note that by default this will initially be undefined
, so if you use this, you need to either check that or set an inital value in the createItem
call.
This could be useful for a lift system. When the player enters the lift, you want to know what floor she is currently on, which you can get from what the previousLoc
value. If the game starts in the lift, you might want to set previousLoc
to "lobby", say, to have the lift start the game on that floor.
Fake locations
Just occasionally you might want to move an item to a fake location. Locations are just stored as strings, so you should be able to just set that to a location that does not exist, right? No! QuestJS will assume that is a bug, and will complain.
What you need to do is tell Quest about these fake places in advance. In the settings.js file, have something like this:
settings.placeholderLocations.push('my fake location')
Other
For how display verbs are handled, see here.
scenery
Just as with Quest 5, setting this to true
makes the object scenery, so it will not appear in a room description (say when the user does LOOK), but the player can still interact with it. You can also set it to a string, in which case that string will be appended to the room description, rather than the item being listed.
Here is an example.
createItem("flower", TAKEABLE(), {
loc:"garden",
scenery:'A flower seems to be winking at you.',
examine:"A sunflower you think.",
attachable:true,
})
By default the scenery item string is added to the final paragraph of the room description. If you want it to be on a new line for one item, put a vertical bar, |, at the start of the string. If you want them all to be on a new line, set:
settings.sceneryOnNewLine = true
If the player picks up an item, this attribute will automatically be set to false
(whether it was previously true
or a string).
As an alternative to using a string attribute, it can be a good idea to check the value of this attribute in a room description via the text processor, so the item only gets mentioned when the attribute is true. When the player picks it up, scenery
is set to false, and the room description no longer says it is there. It is more complicated, but does offer more flexibility.
The shed is disappointingly empty{if:flashlight:scenery:, apart from a torch in the far corner}.
Whatever way you do it, if there is any other way to pick up the item, ensure you set "scenery" to false
there. If you do not it will disappear when the player drops it, which can be a very mysterious bug to resolve!
By default, items flagged as scenery do not appear in the lists in the side panes. However, setting settings.showSceneryInSidePanes
to true
will change that; items will still not appear in the list for rooms, but will appear in the side panes. This is important if you have no command line in your game.
examine
This can be a string or a function, and will be used when an item is looked at.
Note that locations use "desc" while items use "examine".
If this is a function, it can be given an options parameter; a dictionary that contains the "char" attribute, who is doing the examining, and "item", the item itself. This is useful for using the text processor; you can just pass the dictionary straight to msg
.
Some templates (such as ROPE) use examineAddendum()
to automatically append the state of the rope to the description. However, this means you need to set up your examine attribute to allow for that. If the "examine" attribute is a string, Quest an handle automatically. If it is a function you can either have the function return a string instead of printing it itself, or use the "add" attribute of the options parameter.
examine:"The rope is about 40' long.",
examine:function() {
return "The rope is about 40' long."
},
examine:function(options) {
msg("The rope is about 40' long." + options.add)
},
Note that in "dev" mode, Quest will check the "examine" attribute of all items to verify they exist and to some degree work. This means that if you have anything triggered when an item is examined, it will happen too soon. The solution is instead to use "afterExamine". This code will show a conversation topic when the rotten carrot is examined, and illustrates how you can prevent that happening too soon by checking world.isCreated
.
examine:"A carrot that looks to be rotten.",
afterExamine:function() {
w.Lara_bad_carrot.show()
}
If your "examine" function depends on things being set in the world (such as player
or currentLocation
), it may be best to have it do nothing during the testing.
examine:function() {
if (!world.isCreated) return
// ... do stuff
},
As Immutable Strings
What are immutable strings? Simply strings that you do not change during the course of the game. Consider a point in the game where the player has to paint a box blue. One way to handle that would be to change the "examine" property of the item when it is painted. You are now changing the string, rather than using an immutable string. A better way, in my view, would be via a text processor directive. The string does not change, but the output from it does.
If you are creating a large game, I would suggest you only use immutable string descriptions (and function descriptions), and tell Quest not to save them. This will significantly reduce the size of game files. You can tell quest not to save them, by adding this line to settings.js.
settings.saveLoadExcludedAtts.push("examine")
Note that Quest will not warn you if you do change an immutable string!
Based on state
If the item can be in various states, one approach is to give it a array of descriptions, and have the "examine" string use the text processor to pick one.
Let us suppose a robot that, over the course of the game, starts as a set of parts, is then assembled into a working robot, then turns evil, and finally is destroyed. We will track the progress of the robot in an attribute called "state" which starts from 0 and rises to 3, and put the descriptions in a list called "descs". The "examine" then uses the "select" text directive to pick the right one.
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}",
}
examineAddendum
This is a function that should return a string that will be added to the description. It is designed to be used by templates to add the current state of the item. For example, the ROPE template uses this to add the state of the rope (what it is tied to) to its description.
examine_count
Counts the number of times the item has been examined. Note that if the item has never been examined, this will not exist (be undefined
).
parserPriority
If the parser cannot decide if the user is referring to a number of different items, the disambiguation menu is displayed. Sometimes we can guess in advance, and you can use this attribute to encourage or discourage the parser to pick this item when decided what the user is referring to. Use a positive value to have the parser tend towards this item (perhaps 10) and a negative to indicate it should tend not to (say -10).
excludeFromAll
If the user does GET ALL, any item with excludeFromAll
set to true will be skipped. This is the case for NPCs and items flagged as scenery by default.
takeable
, player
, room
, wearable
, countable
, etc.
Most templates set a Boolean flag, so items can be identified as that type. Should not be changed!
lightSource()
A function, defaults to return LIGHT_FULL for rooms and LIGHT_NONE for items.
icon()
Returns the file name of the item's icon. As this is a function, the name returned could depend on the item state, for example, whether a container is open or closed.
goInDirection, goOutDirection, goUpDirection, goDownDirection, goThroughDirection
Occasionally a user will want to refer to an item to go somewhere. Examples include CLIMB TREE, GO IN HOUSE, GO THROUGH PORTAL.
To facilitate this, you can give the object a go-direction attribute. This should be the name of a direction. For the examples above that might be:
goUpDirection = 'up',
goInDirection = 'east',
goThroughDirection = 'north',
Taking the tree example, if the user types CLIMB TREE, Quest will check the "goUpDirection", find it is set to "up" and so will use the "up" exit for the current location (and throw an error if there is none). The exit will be handled exactly as if the player typed UP.
Here is a complete example that also adds a display verb.
createItem("cage_in_square", {
alias:"cage",
examine:"The cage is about six foot along each side and maybe seven foot high.",
goInDirection:'in',
verbFunction:function(list) {
list.push("Enter")
},
loc:'demon_prince_parlour',
scenery:true,
})
Generally, this should not be used with an item the player can carry around, as the current location could be anywhere. However, the attribute is saved, so you could have a ladder, and set "goUpDirection" as appropriate (in the example below, when the "up" exit for a room has "ladder" set to true
) when it is put down, and delete the attribute otherwise. The exit should also check if the ladder is in place, as the player could just type UP.
afterMove:function() {
if (w[this.loc].up && w[this.loc].up.ladder) {
this.goUpDirection = 'up'
}
else {
delete this.goUpDirectionp'
}
},
transform
Generally when the state of an item changes, it is best to change an attribute of the item, and have everything refer to that attribute.
If it is a big change, however, it may be easier to transform the item into a different one. For example, when you animate a body, it might be just an ordinary item before hand, but afterwards in has the NPC template.
To handle this, use "transform".
this.transform(w.animated_body)
The new item will be given the location of the old, whilst the location of the old is deleted. Any item with the first as the location will be moved to the second (which would include components, clothing, anything carried). The parser will also understand "it" to refer to the new item, as appropriate.
If the change is into something different (such as making a funnel from a sheet of paper), the CONSTRUCTION template may be more appropriate - see here.
Notes
When an item is created
You can use settings.itemCreateFunc
to do something each time an item is created. You could use this to set an attribute on every item, in the example the zone is set to the current value of the global variable 'zone'. You could include a counter too.
settings.itemCreateFunc = function(o) {
o.region = region
}