Using ZONE to Handle Large, Open Areas - ThePix/QuestJS GitHub Wiki
A zone is significant number of locations (at least ten, more likely dozens or even hundreds) that the player can traverse more or less without restriction (i.e., can go any compass direction from most locations). An example would be a desert or savannah; the player would expect to be able to head off in any direction. Each location has a position in a grid, so a player going n, then e, then s and then w (or ne, nw, sw, se) will trace out a square, returning to the original location. While a lot of locations are readily made, their descriptions will be similar or even identical.
Note that NPCs may be unable to navigate their own path across a zone - they can still follow the player or a location by location script.
How do we create a zone?
Zones are in an optional library, so the first step is to tell Quest to include it. Somewhere in your settings.js file add this line:
settings.libraries.push('zone')
In QuestJS you can create a zone very easily using a single location, by giving it the ZONE template. Here is a simple example for a desert.
createRoom("desert", ZONE(), {
descs:[
{
desc:'You are stood in the desert.'
},
],
})
Note that you can do a "desc" attribute as normal for a ZONE, but every location will have the same description. We will use a special "descs", which will also give every location the same description for now, but as we add more to the ZONE, we will get more and more variation.
The "descs" attribute is an array of dictionaries. Each dictionary (there is only one so far!) needs a "desc" attribute of its own.
Quest tracks the player's position as co-ordinates, using two attributes, positionX and positionY (as you go east, x increases; as you go north, y increases). You should ensure both these are set before the player goes into the desert (they will default to 0, 0 otherwise). The easiest way (assuming a single point of entry to the desert) is to set them up in the player object at the start.
createItem("me", PLAYER(), {
loc:"temple",
regex:/^(me|myself|player)$/,
positionX:5,
positionY:0,
})
At some point in your game, have an exit going to "desert", and that will take the player to the desert. A very boring desert; every location is identical, but if you drop an object in a location, you will be able to leave it and come back to it. It is also infinite in all directions, and there is no way out of it.
Features
Let us put a tower in the desert. A tower will be visible from some distance away, so we will flag this as a feature of the zone.
createItem("tower", ZONE_FEATURE('desert', -1, 3, 4), {
featureLook:"There is a tower to the #.",
featureLookHere:"There is a tall stone tower here.",
zoneMapName:'Ancient tower',
examine:"The tower looks ancient, but in a fair state of repair. It is about four storeys high.",
})
The ZONE_FEATURE template requires four parameters; the first is the name of the zone (the room we created earlier). The next two given the x and y co-ordinates of the location the feature is in. The fourth is the range; how close the player has to be to see the feature. In this case she will be able to see the tower if within 4 squares.
If the player is within range, the "featureLook" text will get added to the location description, and the # replaced by the general direction of the feature.
You are stood in the desert. There is a tower to the southwest.
If the player is in the same location as the feature, the "featureLookHere" text is used instead.
You are stood in the desert. There is a tall stone tower here.
The "examine" text is used if the player does EXAMINE TOWER, as with any item.
You can have as many features as you want; this could be a way to help the player navigate he wilderness; head north to the tower, then west to the oasis, etc.
Note that features can be dynamic. You could change the co-ordinates to have the feature move around the zone, perhaps chasing or evading the player. This will not help her navigate the wilderness!
Borders
You will likely want to limit the extent of the wilderness (otherwise some players will inevitably go stupid distances and get themselves lost). We will add two, but you can have as many as you like; a canyon to the southeast and a circular invisible barrier around everything.
Borders are items with the ZONE_BORDER template, and some special attributes.
createItem("barrier", ZONE_BORDER('desert'), {
examine:"It is invisible!",
scenery:true,
border:function(x, y) {
return (x * x + y * y > 49)
},
borderMsg:"You try to head #, but hit an invisible barrier.",
borderDesc:"The air seems to kind of shimmer.",
})
createItem("canyon", ZONE_BORDER('desert'), {
examine:"It looks very deep!",
scenery:true,
border:function(x, y) {
return (x - y > 5)
},
borderDesc:"There is a deep canyon southeast of you, running from the southwest to the northeast.",
})
The template needs to be given the name of the zone, as usual. "borderDesc" will be added to the location description when the player is adjacent to the border (and is optional). I have chosen to flag both as scenery.
There are two types of border - obvious and hidden. If the border has a "borderMsg" attribute, then it will be a hidden border. It will look like the player can go that way (it will be active in the compass rose, and listed as an available exit) until she tries to, at which point a message is displayed saying she cannot using the "borderMsg" attribute. If there is no "borderMsg" attribute, the player will not be told that that direction is an exit, and it will not be active in the compass rose; if the player nevertheless tries to go that way, she will get the standard "can't go that way" message.
The complicated bit is the "border" attribute. This is a function that takes the x and y co-ordinates, and must return true
if the given location is outside the border and false
if it is inside. This does require some maths...
The first border above uses Pythagoras! If x squared plus y squared is greater than 7 squared, return true; i.e., if the location is more than seven away from the origin, it is out-of-bounds (in fact, a better shape is produced if you use 7 and a bit squared - say 55 instead; later we will produce a map, and once you have that, experiment with different values)
The second defines a region to the southeast the player cannot go. The border is a diagonal line, running from sw to ne. It can be helpful to consider what happens if the player goes due north, south, east or west when thinking about these. If the player heads east, then x will increase, but y stays at zero. As x rises to 5, the function will return false
, but at 6 it returns true
; at that point the player has hit the border. Similarly when heading south, the player will hit a border when y is 5. If you draw a line between these two, you can see where the border is.
Your border function could depend on the state of an object in the game. Perhaps one area is only accessible once the rockfall is cleared.
For the invisible barrier, you might want it only in the location description once the player has tried to go though it. we can update the "border" function to set a flag, encountered
, indicating the player has tried to go through it. This is slightly complicated as the function is used other times, so we need to check the third parameter is true
. Then we can wrap the "borderDesc" in a text processor directive that checks encountered
.
border:function(x, y, actual) {
const flag = x * x + y * y > 85
if (flag && actual) this.encountered = true
return flag
},
borderDesc:"{if:barrier:encountered:There is some kind of invisible barrier here.}",
Exits
We need at least one way for the player to escape the desert. Perhaps, in addition, the player can enter the tower with "in", and let us suppose there is a bridge over the canyon due east of the origin. To do that we need to go back to the "desert" object, and add a "zoneExits" attribute.
createRoom("desert", ZONE(), {
zoneExits:[
{x:-1, y:3, dir:'in', dest:'inside_tower', msg:'You step inside the tower, and climb the step, spiral staircase to the top.'},
{x:5, y:0, dir:'east', dest:'bridge', msg:'You start across the bridge.'},
],
descs:[
{
desc:'You are stood in the desert.'
},
],
})
The "zoneExits" attribute is an array of objects, so you can have as many as you like. Each exit needs x and y co-ordinates to define its position - the same as the tower - plus a direction, "dir". You also need to set the destination with "dest" - the name of the room the player will go to. The "msg" is the text that will be shown.
Note that the direction must be all lowercase, the full word, no spaces.
It is worth discussing the exit going the other way. If the bridge location is given a "west" exit going to "desert", then you are good to go. However, if there are multiple access points to the zone and the player can leave from one and return to a different one, or if you have more than one zone, you have a bit more work to do, as you need to ensure that when the player goes into the zone, the x and y co-ordinates are right. You could do that in various places, for example in the "use" script of the exit that takes he player to the zone, but the easiest is to do it in a "beforeEnter" script on the location the player is going from. Here is an example:
createRoom("bridge", {
desc:'From the bridge you can just how deep the canyon is.',
west:new Exit('desert'),
east:new Exit('road'),
beforeEnter:function() {
game.player.positionX = 5
game.player.positionY = 0
},
})
Items
If you want to have ordinary items in the zone at the start of your game, you should give them the ZONE_ITEM template, with the parameters being the name of the zone, and the x and y co-ordinates. Note that you should not give a "loc" attribute. An example:
createItem("silver_coin", TAKEABLE(), ZONE_ITEM('desert', 1, 1), {
examine:"A curious silver coin; you do not recognise it. It says it is worth two dollars.",
})
If the zone is big, the chances of the player just stumbling across an item is small, so plan accordingly.
Better descriptions
We already are using "descs" for the location descriptions, we can add to that to give more variety.
The "descs" attribute is an array of dictionaries, and each dictionary has its own description ("desc" attribute") and, except the last, a rule for when to use it. Quest will use the first description it finds for which the rule fits. This example has four elements in the array:
createRoom("desert", ZONE(), {
zoneExits:[
{x:-7, y:4, dir:'in', dest:'inside_tower', msg:'You step inside the tower.'},
{x:-7, y:4, dir:'east', dest:'bridge', msg:'You start across the bridge.'},
],
descs:[
{
x:5, y:0,
desc: 'You are stood on a road heading west through a desert, and east over a bridge.'
},
{
when:function(x, y) { return y === 0 },
desc:'You are stood on a road running east to west through a desert.',
},
{
when:function(x, y) { return y > 0 },
desc:'You are stood in the desert, north of the road.',
},
{
desc:'You are stood in the desert, south of the road.',
},
],
})
The first element has "x" and "y" attributes; it will be used only when both match the player's current position. Note that if you have x but not y, you will get an error, if you have y but not x, it will be ignore. You need both.
The second and third have a "when" function; the description will be used when the function returns true.
The last has no conditions; it is the default.
Note that a description can be a function that returns a string. The function will be passed the x and x values. Either way the features will be appended.
How complicated you make it is up to you. It has the potential to get very big; this example is relatively simple. However, you can start small, and gradually add new descriptions and rules to get a more varied world.
Drawing a map
Zones have a built-in map facility, so drawing a map of the zone is trivial; just add a "size" attribute.
createRoom("desert", ZONE(), {
zoneExits:[
{x:-7, y:4, dir:'in', dest:'inside_tower', msg:'You step inside the tower.'},
{x:-7, y:4, dir:'east', dest:'bridge', msg:'You start across the bridge.'},
],
size:10,
...
The map is assumed to be centred at 0,0, and to extend "size" locations in all four directions. Go into your game, navigate to the zone, and type MAP. You should see a yellow blob - all the locations the player can access - surrounded by light grey. Each feature should appear as a blue circle, and the player as a black square.
The map is useful for checking your zone is set up how you think it should be (the borders and features are in the right place) so is worth turning on whilst testing even if you do not want the player to access it - just delete the "size" attribute to prevent the player using it. Note that you could add the attribute during play to allow the player to see the map after a certain point in the game.
If the borders and features change during play, the MAP command will display the situation at that moment.
If the player can access the map, you have various options for customising it. You can modify various defaults in the zone object.
createRoom("desert", ZONE(), {
zoneExits:[
{x:-7, y:4, dir:'in', dest:'inside_tower', msg:'You step inside the tower.'},
{x:-7, y:4, dir:'east', dest:'bridge', msg:'You start across the bridge.'},
],
size:10,
insideColour:'green', // Locations the player can access
outsideColour:'transparent', // Locations the player cannot access
mapBorder:false, // Hide the map border
featureColour:'blue', // Default colour for features
playerColour:'black', // Colour of the player
cellSize:20, // The size of each location, if less than 10 the player will disappear!
mapFont:'italic 10px serif', // Style of the labels for features
...
You can change the colour of a feature and give it a label using "zoneColour" and "zoneMapName" attributes, for example:
createItem("cactus", ZONE_FEATURE('desert', -1, 4, 2), {
featureLook:"There is a big cactus to the #.",
zoneColour:'green',
zoneMapName:'Strange cactus',
examine:"Prickly!",
})
SVG is an extension to HTML, so you can use any colour that is understood by HTML, which is over a hundred named colours, or use the format #rrggbb
or #rgb
(or "transparent").
You can also add your own SVG elements to the map using the three attributes "mapCells", "mapFeatures" and "mapLabels". Each of these is an array of strings, with each string defining one SVG element. Elements in "mapCells" with appear over the map, but under the features, whilst those in "mapFeatures" will appear over the features, but under the labels, and those in "mapLabels" will appear over the labels but under the player.
This simple example draws a grey rectangle representing the east-west road.
createRoom("desert", ZONE(), {
zoneExits:[
{x:-1, y:3, dir:'in', dest:'inside_tower', msg:'You step inside the tower, and climb the step, spiral staircase to the top.'},
{x:5, y:0, dir:'east', dest:'bridge', msg:'You start across the bridge.'},
],
size:10,
desc:function() {
return 'You are stood in the desert.' + this.getFeatureDescs()
},
mapCells:[
'<rect x="0" y="162" width="336" height="16" stroke="none" fill="#999"/>'
],
})
SVG is a complex technology, more here