Template: TRANSIT and TRANSIT_BUTTON - ThePix/QuestJS GitHub Wiki

This is applicable to any transport systems that has a specific set of destinations, such as a bus or a ferry. You could even apply it to a magic portal. In this first example, we will build a lift that can go from ground floor to the second floor (in the US, you would have an elevator going from first to third, but please bear with us...). This will use buttons, but we will look at alternatives later.

Destination from buttons

This is a simple system. It is assumed the lift is always on the same floor as the player - that is, if the lift is north, and the user types NORTH, the player steps directly into the lift, without pressing the call button and waiting for the lift to arrive. It could be extended to force the player to press the "call lift" button, and then wait for the lift to arrive, but, while that would be more realist, it is unlikely to make the game more fun, so we will not bother here.

A limitation of this system is that the lift can have only one way out. You cannot have a lift where you can enter it from the west, go to another floor, then exit to the east. This is how most lifts are, but does not work so well for a bus or train, and you might want to use in/out.

So the first thing we need is the lift itself. This is unusual for a room as it has a template, TRANSIT. The template needs to be given the direction of the lift door. Besides that, the room needs an exit, obviously in that direction. Here is an example:

createRoom("lift", TRANSIT("east"), {
  desc:'A curious lift.',
  east:new Exit('dining_room'),
})

Now we need some buttons to move the lift. These use the TRANSIT_BUTTON template.

createItem("button_g", TRANSIT_BUTTON("lift", "dining_room"), {
  alias:"Button: G",
  examine:"A button with the letter G on it.",
})

The TRANSIT_BUTTON template needs two attributes, the name of the lift (the "loc" attribute will be set to this so you do not have to) and the name of the destination (for the "transitDest" attribute).

Two further important attributes are:

  • "transitAlreadyHere", pressing the button when the lift is already on this floor will give this message.
  • "transitGoToDest", pressing the button when the lift is going to this floor will give this message (can be a function too).

These will use defaults set in lang-en.js, but I would recommend setting them, as in this example (which sets it to the defaults!).

createItem("button_g", TRANSIT_BUTTON("lift", "dining_room"), {
  alias:"Button: G",
  examine:"A button with the letter G on it.",
  transitAlreadyHere:"You press the button; nothing happens.",
  transitGoToDest:"You press the button; the door closes...",
})

You can have as many buttons as you like.

That is it, you now have a working lift. Of course, they are plenty of options you could use...

Checking and changing state

Use the getTransitDestLocation() function of the transit location (e.g., the lift, w.lift in the example) to get the name of the location the transit is current at (e.g., "dining_room" if it is on the ground floor), or use getTransitDestButton() to get the button object (e.g., w.button_g).

Use isTransitHere(char) to determine if the transit is at the location the given character is at (defaults to the player if not given). Probably only useful if you are implementing a call button.

Use setTransitDest(button) to set the transit current location; note that this needs the button object. You can use findTransitButton(loc) to find the button, for example like this:

w.lift.setTransitDest(w.lift.findTransitButton("dining_room"))

Conditional buttons

You can set a button to be locked (locked:true), and when the player tries to use it the "transitLocked" attribute gets displayed. You can set a button to be hidden, and have it appear later in the game.

Alternatively, give a button a "testTransitButton" function. If the button can be used, it should return true. otherwise it should print a message and return false.

You can also give the transit itself a "testTransit" function that works like "testTransitButton", but applies to the entire system; useful if the player has to first get power to the lift or whatever.

transitDest text processor directive

Usually a lift will have something inside it indicating the floor it is on. You can add that to the description using the transitDest text processor directive. If this is used whilst the player is in the lift, Quest will automatically assume you mean that transit location.

createRoom("lift", TRANSIT("east"), {
  regex:/elevator/,
  desc:function() {
    return 'The lift is small; according the plaque it is limited to just three people. There are three buttons, labelled one to three. A label above indicates the lift is at "{transitDest}".'
  },

You can also use it for the "transitGoToDest" of a button, to tell the user where the lift is heading:

  transitGoToDest:"You press the button; the door closes and the lift moves to the {transitDest} floor.",

For situations where the player will not be in the lift (perhaps she is stood waiting for it), you can add the name of the location as an option.

The lift is at "{transitDest:lift}".

By default the alias of the room the lift currently exits to will be displayed, but you can use the "title" attribute of a transit button to specify other text.

Destination from options

Let us suppose the lift is old-fashioned, and has a guy in it to operate it (this is also a better way to handle a bus or ferry). As the player steps in, he asks her where she wants to go. We still want the buttons, as they tell the system where the lift can go (though if you are building this from scratch you would probably do it another way!).

The menu will appear after the player enters the room, so this needs to be in the afterEnter script. There is a helper function that does all that for us, transitOfferMenu. We also need to set the "transitMenuPrompt" attribute.

createRoom("lift", TRANSIT("east"), {
  desc:'A curious lift.',
  east:new Exit('dining_room'),
  transitMenuPrompt:'Where do you want to go?',
  afterEnter:transitOfferMenu,
})

For the buttons, we have one new attribute, "transitDestAlias", which is how the destination will appear in the menu. The button is now hidden as the player cannot use them herself. You can set a button to be locked to prevent it appearing in the list of destinations offered to the player.

createItem("button_g", TRANSIT_BUTTON("lift"), {
  hidden:true,
  transitDest:"dining_room",
  transitDestAlias:"Ground floor",
  transitAlreadyHere:"You press the button; nothing happens.",
  transitGoToDest:"You press the button; the door closes and the lift heads to the ground floor. The door opens again.",
})

Tickets, please!

Let us suppose this is a bus. In this variation we have "transitAutoMove" set to true, which will move the player straight out of the bus again.

We also have the afterTransitMove attribute set to a function. This fires when the player moves, after the "transitGoToDest" attribute has been displayed (and not at all if the "transitAlreadyHere" attribute has been displayed!). All it does is reduces the player's cash by 25, the cost of the ticket. Parameters are given so you could potentially set the cost depending on the distance, but whether that is worth doing is questionable.

That means we have to stop the player getting on the bus if she does not have enough cash. For that we have the testTransit function, which fires before the menu is offered (note that it is not used if the player presses a button). If all is well, it should return true. If not, it should give a message and then return false.

createRoom("bus", TRANSIT("east"), {
  desc:'You step up into the bus.',
  east:new Exit('bus_station'),
  transitMenuPrompt:'Where do you want to go?',
  afterEnter:transitOfferMenu,
  transitAutoMove:true,
  afterTransitMove:function(toLoc, fromLoc) { player.cash -= 25; },
  testTransit:function() {
    if (player.cash < 25) {
      msg("You do not have enough money")
      return false
    }
    return true
  },
})

An alternative way to restrict access to the bus would be to have a script on the exit leading to it do the job. You would need to do that on every exit that goes to the bus.

Rooms inside the transit

You can add rooms to the transit location, allowing the player to, for example, walk up the train to the restaurant. Note that if you are using the built-in map system, this will not work (you could add it yourself, but it will not be simple).

Events

The "afterTransitMove(toLoc, fromLoc)" event will be called on the transit location when it moves.

This example will have a message fire the first time player has the transit go to a location with "portland" in the name

  afterTransitMove:function(toLoc, fromLoc) {
    if (toLoc.match(/portland/) && !this.messageReceived) {
      this.messageReceived = true
      msg("You hear an alert from your phone.")
    }
  },

Where You Left It

If the transit is a car, then it will only be where the player left. If the car can be parked at various spots, you only want one to have the option to get into the car. The way to handle this is to make the exit into the car hidden if the car is not there, and there is a utility function you can use for that.

  in:new Exit("car_interior", {isHidden:util.hiddenIfNoTransit}),

You will also need the car to be present only if it is where the player left it. I think it is easy to have a different object to present the exterior of the car; this will be an item rather than a room. We can give it a "isLocatedAt" function to handle whether it is at the location or not.

createItem("car", {
  isLocatedAt:function(loc) {
    return (w[loc].name === w.car_interior.transitCurrentLocation)   
  },
  scenery:true,
  goInDirection:'in',
  examine:"A sporty red car.",
})

A Modular Game

The transit system lends itself well to building a modular adventure, in which new regions are added at a later date and/or created by other authors.

Each region would be defined in its own file (or set of files), and would include the button for the transit. Adding the file to the list of files in settings.js will add it to the game world. Because of the way games are saved during play, your users will be able to access the new region with characters that have already explored the earlier regions. Just head to the transit system; the new button will be there and useable.

Other Attributes

Be aware that the transit location has its own "beforeEnter" and "templatePostLoad" functions, while the buttons have "push" functions. The location also has a number of custom functions, but all have "transit" in the name. You are otherwise free to add your own custom functions as with any ordinary location, item or exit.

Advanced Coding...

If you are feeling adventurous...

An advantage of the techniques described below is that if decide you want to set up the lift differently, you should just have to make a single change, rather than changing each and every button or exit. Of course, if you only have two, it may not be worth it.

Buttons in a loop

As the buttons are very similar, you might want to create them in a loop. That is, you set up a list that you iterate through using a for loop, and create a button for each entry.

This example sets up two lists, the first is the name of the destinations on each floor. The second is the alias - what the player will see. Then there is the loop, which runs createItem for each entry. It counts down so the buttons appear in the side pane highest at the top.

const liftDests = ['lobby', 'shops', 'entertainment', 'sun deck']
const liftDestNames = ['Lobby', 'Shopping', 'Entertainment', 'Sun Deck']

for (let i = liftDests.length-1; i >= 0; i--) {
  createItem("button_" + (i+1), TRANSIT_BUTTON("elevator"), {
    alias:"Button: " + (i+1) + ' (' + liftDestNames[i] + ')',
    examine:"A button with the number " + (i+1) + " on it.",
    transitDest:liftDests[i],
    title:'Deck ' + (i+1) + ': ' + liftDestNames[i],
    transitAlreadyHere:"You press the button; nothing happens.",
    transitGoToDest:"You press the button; the door closes ...",
  })
}

Here is another version that searches through all the locations in your game and connects the lift to any that have a "liftDestName" attribute (which should be a string). It will add that location as a button for the lift, and will also add an exit to that location going into the lift.

let count = 1
for (const key in w) {
  const o = w[key]
  if (o.liftDestName) {
    createItem("button_" + o.name, TRANSIT_BUTTON("elevator"), {
      alias:"Button: " + count + ' (' + o.liftDestName + ')',
      examine:"A button with the number " + lang.toWords(count) + " on it.",
      sidebarButtonVerb:'push',
      transitDest:o.name,
      transitDestName:o.liftDestName,
      title:'Deck ' + count + ': ' + o.liftDestName,
      transitAlreadyHere:"You press the button; nothing happens.",
      transitGoToDest:function() {
        msg("You press the button; the door closes and the elevator moves to the {show:button:transitDestName} floor.", {button:this})
      },
    })
    o.in = new Exit("elevator")
    count++
  }
}

Note that this code must be after all the locations have been created. Also, buttons will appear in the side pane in the order they appear in files; I suggest therefore having the top floor first, and working down the levels.

If you later decide you want to add an additional storey, just create a new location with a "liftDestName" attribute, and you are done. You can even do this after you release your game. Users will be able to load a saved game, and access the new area.

You might want to consider having each level in is own file.

Exits with a sub-class

If the exits going into the lift all do something odd, but the same thing, you could create a new class that extends the existing Exit class. A good example would be if you only want the lift useable if on the right floor.

The extends keyword tells JavaScript this is a new class, but it gets all the built-in stuff from Exit. The "constructor" function is a special one because it is called when the thing is created, specifically when new is used. And the first thing it does is call super, another special function, which indicates the constructor in the original class. In this case, we are sending the destination of the exit, which is the same for all these exits. We then set some other attributes.

class LiftExit extends Exit {
  constructor() {
    super("lift")
    this.alsoDir = ['southeast']
    this.msg = 'She steps into the lift.'
  }

  simpleUse(char) {
    log('here')
    if (w.lift.getTransitDestLocation() !== this.origin) {
      msg("The door is locked.")
      return false
    }
    return util.defaultSimpleExitUse(char, this)
  }
}

The other function here is simpleUse. Note that function definitions are slightly different inside a class definition - they do not use the function keyword; it is implicit.

To create an exit to the lift for a location, we just do this:

  in:new LiftExit(),