The Cloak of Darkness - ThePix/QuestJS GitHub Wiki

The Cloak of Darkness is a specification for an adventure game that has been created in numerous systems, with the purpose of giving prospective authors some idea of what is involved in each system.

IFWiki says of it:

This adventure is a tiny adventure designed to be easy to port to a given Authoring system. It is, if you will, the interactive fiction equivalent of “Hello, world!”

There are three versions written for Quest. For Quest 3.5, see here (source code here); this was written in October 2003 and if you look at the source code you will see it is very different to the version of Quest that we know today. [As of May 2020, these links do not work properly; I will leave them in place inthe hope this is temporary]

A version for Quest 5 can be found here, and I will be freely coping from that here (I wrote it, so I am allowed to!).

The QuestJS version can be found here.

The Specification

The specification can be found here, but for convenience I will repeat it in full:

  • The Foyer of the Opera House is where the game begins. This empty room has doors to the south and west, also an unusable exit to the north. There is nobody else around.
  • The Bar lies south of the Foyer, and is initially unlit. Trying to do anything other than return northwards results in a warning message about disturbing things in the dark.
  • On the wall of the Cloakroom, to the west of the Foyer, is fixed a small brass hook.
  • Taking an inventory of possessions reveals that the player is wearing a black velvet cloak which, upon examination, is found to be light-absorbent. The player can drop the cloak on the floor of the Cloakroom or, better, put it on the hook.
  • Returning to the Bar without the cloak reveals that the room is now lit. A message is scratched in the sawdust on the floor.
  • The message reads either “You have won” or “You have lost”, depending on how much it was disturbed by the player while the room was dark.
  • The act of reading the message ends the game.

As you can see, there is really not much to it! And in fact, it is really a bit vague in parts; in my opinion it would be better if it stated exactly what should happen - even going so far as giving a handful of walk-throughs with the expected responses. The Quest 5 documentation discusses where it is less clear; I am adopting the same understanding here as there.

So how would one go about creating that in QuestJS?

The settings.js file

The settings.js file loads up early; not quite first, but before most of the library files. This is where we set up the game environment.

Here is the file for this game:

"use strict"

// About your game
settings.title = "Cloak of Darkness"
settings.author = "The Pixie"
settings.version = "0.3"
settings.thanks = []
settings.ifid = '1F95711E-44DE-4C57-AA30-0BAB292E5874'

settings.panes = 'none'

settings.roomTemplate = [
  "#{cap:{hereName}}",
  "{hereDesc}",
  "{objectsHere:You can see {objects} here.}",
]

The first line just says we want to use a safer version of JavaScript, and is at the top of all our files.

Then some meta-data is set in the next five lines. The side panes are turned off.

Then there is the template for the room descriptions. This is an array of strings, and I have chosen not to list exits, but to have room names as headings. The square brackets demark the array, and the double quotes demark each string, with one string per paragraph. Each string uses a text processor directive to have the right text inserted when the template is used - more on the text processor here.

We cannot do much at all in the settings.js file except set the settings as the game world does not exist yet and libraries have not been loaded.

The styles.css file

This is where the style of the page is set - what font to use, the colours, etc. Most of this is done elsewhere - QuestJS has a file that sets the defaults; in this file we just need to add what we want to change to make our game different.

This uses a language called Cascading Style Sheets (CSS), rather than JavaScript - that is just how web pages do it.

@import url('https://fonts.googleapis.com/css2?family=Rozha+One&display=swap');

body {
  color: #333;
  font-family: 'Rozha One', serif;
  background-color: white;
}

The first line says that the browser is to download a font to be used on the page. There is just one CSS selector used, "body", which will affect the entire page. The set of attributes follows the name in curly braces, in this case, the colour of the text, the font and the background colour.

More on this can be found here.

The code.js file

By default, QuestJS assumes everything else will be in code.js and data.js, and by convention locations and items are defined in the latter and everything else in the former; this is how this game is arranged. However, you can split your game into as many files and how you like. You might want to put NPCs in their own file, or split the game up by region.

In code.js we will set up a few things we will use later.

This is the first half of the file:

"use strict"

const cloakHere = function() {
  if (w.cloak.isAtLoc('me')) return true
  if (w.cloak.isHere()) return true
  if (w.cloak.isAtLoc('hook') && game.player.isAtLoc('cloakroom')) return true
  return false
}

lang.no_smell = "It smells slightly musty."

lang.no_listen = "It is quiet as the grave..."

tp.addDirective("cloakHere", function(arr, params) {
  return cloakHere() ? arr[0] : arr[1]
});

There are five sections to this, the first being the line to say we are using the safer version of JavaScript as before.

The second creates a new function that we can use later. Because the game revolves around the eponymous cloak, it is useful to have a function that can tell us if it is here, this is called "cloakHere". There are three situations when the cloak is present: the player has it; it is in the same location as the player; it is on the hook while the player is in the cloakroom. In each event, the function returns true. Otherwise it returns false.

The third and fourth bits replace default responses in the lang object, which holds all the English language stuff, and are just strings.

The fifth bit adds a custom directive to the text processor. It uses the function we defined earlier to decide whether to print the first or second string. Here is the description of the cloakroom, andyou can see it in action.

"The cloakroom is {cloakHere:dimly:brightly} lit, and is little more than a cupboard. "

If the cloak is here, the word "dimly" is used, otherwise "brightly" is used.

The code.js file - commands

I have also modified ABOUT and HELP. Here is the code; in each case, it finds the existing command, and gives it a new "script" attribute.

findCmd('MetaCredits').script = function() {
  metamsg('This game was created by The Pixie, following the Cloak of Darkness specification by Roger Firth.')
}

findCmd('MetaHelp').script = function() {
  metamsg('Just type stuff at the prompt!')
}

There is one new command - HANG UP. This is more complicated as we create a new Cmd object.

new Cmd('HangUp', {
  regex:/^(?:hang up|hang) (.+?)(?: on the hook| on hook|)$/,
  objects:[
    {scope:parser.isHeld},
  ],
  script:function(objects) {
    if (!objects[0][0].isAtLoc(player)) return failedmsg ("You're not carrying {sb:obj}.", {obj:objects[0][0]})
    if (objects[0][0].worn) return failedmsg ("Not while you're wearing {sb:obj}!", {obj:objects[0][0]})
    if (!player.isAtLoc('cloakroom')) return failedmsg ("Hang {sb:obj} where, exactly?", {obj:objects[0][0]})

    objects[0][0].moveToFrom(player, w.hook)
    msg ("You hang {nm:obj:the} on the hook.", {obj:objects[0][0]})
    return world.SUCCESS
  }  
})

There are three attributes to note here. The first is "regex"; this is a regular expression that Quest needs to match against the user's input for the command to be used; this is a relatively complicated one as the player could try HANK CLOAK or HANG UP CLOAK ON THE HOOK, and we need to cover as much as possible. The second is "objects", an array that tells Quest how to handle each capture group in the regular expression - in this case treat it as an object, looking first for something the player is holding. It is worth noting that this allows QuestJS to make educated guesses about what the user is talking about (suppose you are in a room with a box and a guy called Bobby who has a ball, QuestJS will understand TELL B TO PUT B IN B to mean that Bobby should put the ball in the box).

Finally there is the "script" attribute that is executed if the parser decides this is the best matching command. It follows a very standard pattern, first checking various situations where it would fail - the player is not carrying the item, the player is wearing the item, the player is not in the cloakroom. If it gets to the end, the object is put on the hook.

More details on commands can be found here

The data.js file

Now we have everything set up, we can create locations and objects. This is done using createRoom and createItem, conventionally done in data.js. These functions have the same basic form, requiring a name, followed by any number of dictionaries. These dictionaries are going to be the relevant templates, followed by your custom dictionary.

The player

The player object is pretty much the default, except the location has been changed.

createItem("me", PLAYER(), {
  loc:"lobby",
  synonyms:["me", "myself", "player"],
  examine: "Just a regular guy.",
})

We also have the cloak object, a WEARABLE, so we add that dictionary (or a function that supplies it) and set it to be already worn.

createItem('cloak', WEARABLE(), {
  examine:'The cloak is black... Very black... So black it seems to absorb light.',
  worn:true,
  loc:'me'
})

The data.js file - lobby

The lobby is the first room, and the introductory text is here as the "beforeFirstEnter" attribute (I could have done that in settings.intro). The exit north is notable because you cannot go that way, so we use a BarredExit. There is also an "smell" attribute that will be used if the player does SMELL.

The "desc" attribute is a string that is used when the player looks at the room. We can see the text processor directive we defined earlier, "cloakHere". If the cloak is present, the room is described as dark, otherwise it is merely dingy.

createRoom("lobby", {
  desc:"There is something oppressive about the {cloakHere:dark:dingy} {once:room:foyer}; a presence in the air that almost suffocates you. It's former glory has all but faded; the walls still sport old posters from productions that ended over twenty years ago. Paint is peeling, dust is everywhere and it smells decidedly musty. You can see doors to the north, west and south.",
  beforeFirstEnter:function() {
    msg ("You hurry through the night, keen to get out of the rain. Ahead, you can see the old opera house, a brightly-lit beacon of safety.")
    msg ("Moments later you are pushing though the doors into the foyer. Now that you are here it does not seem so bright. The doors close behind you with an ominous finality...")
    msg ("")
  },
  north:new BarredExit('You try the doors out of the opera house, but they are locked. {once:{i:How did that happen?} you wonder.}'),
  west:new Exit('cloakroom'),
  south:new Exit('bar'),
  smell:'It smells of damp and neglect in here.',
})


createItem('posters', {
  examine:'The posters are ripped and peeling off the wall.',
  read:'You spend a few minutes reading about the shows they put on twenty-odd years ago.... {i:Die Zauberflöte}... {i:Guillaume Tell}... {i:A Streetcar Named Desire}. Wasn\'t that a play?',
  scenery:true,
  plural:true,
  loc:'lobby'
})

The "posters" object is scenery and otherwise very simple.

The data.js file - cloakroom

The cloakroom is only complicated in its "desc" attribute, which returns a string that depends on the state of the room. The hook is also pretty simple other than the "examine" attribute.

createRoom("cloakroom", {
  desc:function() {
    let s = "The cloakroom is {cloakHere:dimly:brightly} lit, and is little more than a cupboard. "
    if (w.cloak.isAtLoc('hook')) {
      s = s + "Your cloak is hung from the only hook."
    }
    else if (w.cloak.isAtLoc(this)) {
      s = s + "There is a single hook, which apparently was not good enough for you, to judge from the cloak on the floor."
    }
    else {
      s = s + "There is a single hook, which strikes you as strange for a cloakroom."
    }
    return s + " The only way out is back to the east. "
  },
  east:new Exit('lobby'),
})


createItem('hook', SURFACE(), {
  synonyms:["peg"],
  hookable:true,
  scenery:true,
  examine:function() {
    if (w.cloak.isAtLoc('hook')) {
      msg("An ornate brass hook, with a cloak hanging from it.")
    }
    else {
      msg("An ornate brass hook, ideal for hanging cloaks on.")
    }
  },
  loc:'cloakroom'
})

The data.js file - bar

The bar itself is quite simple.

createRoom("bar", {
  desc:function() {
    if (cloakHere()) {
      return "It is too dark to see anything except the door to the north."
    }
    else {
      return "The bar is dark, and somehow brooding. It is also thick with dust. So much so that someone has scrawled a message in the dust on the floor. The only exit is north."
    }
  },
  beforeEnter:function() {
    w.message.visible = !cloakHere()
  },
  afterExit:function() {
    w.message.count = 0
  },
  north:new Exit('lobby'),
  smell:'There is a musty smell, but behind that, something else, something that reminds you of the zoo, perhaps?',
  listen:'Is there something moving?',
})

The message, however, is a bit more complicated. It has a turnscript attached and is used to track the win or lose status. More about them here.

createItem('message', {
  synonyms:["writing", "note"],
  count:0,
  disturbed:0,
  scenery:true,
  loc:'bar',
  examine:function() {
    if (cloakHere()) {
      msg ("You cannot see any message, it is too dark.")
      return
    }
    if (this.disturbed < 3) {
      msg ("The message in the dust says 'You have won!'")
    }
    else {
      msg ("The message in the dust says 'You have lost!'")
    }
    io.finish()
  },
  read:function() { this.examine() },
  eventPeriod:1,
  eventIsActive:function() { return player.isAtLoc('bar') && !io.finished },
  eventScript:function() { 
    this.count++
    if (this.count > 1) {
      if (this.disturbed === 0) {
        msg ("You think it might be a bad idea to disturb things in the dark.")
      }
      else {
        if (!player.suppress_background_sounds) {
          msg ("You can hear {random:scratching:something moving in the dark:rasping breathing}.")
        }
      }
      this.disturbed++
    }
  },
})

The data.js file - ubiquitous item

Some players will try to examine the walls. We do not want Quest to say there are no walls, so we will implement them. Each has a special "isLocatedAt" function that will return true if the location is a room. Add any synonyms you can think of - “carpet” for example.

createItem('walls', {
  examine:function() {
    if (cloakHere() && player.isAtLoc('bar')) {
      msg("It is too dark to see the walls.")
    }
    else {
      msg("The walls are covered in a faded red and gold wallpaper, that is showing signs of damp.")
    }    
  },
  scenery:true,
  isLocatedAt:function(loc) { return w[loc].room },
})

createItem('ceiling', {
  examine:function() {
    if (cloakHere() && player.isAtLoc('bar')) {
      msg("It is too dark to see the ceiling.")
    }
    else {
      msg("The ceiling is - or was - white. Now it is a dirty grey.")
    }    
  },
  scenery:true,
  isLocatedAt:function(loc) { return w[loc].room },
})

createItem('floor', {
  synonyms:["carpet"],
  examine:function() {
    if (cloakHere() && player.isAtLoc('bar')) {
      msg("It is too dark to see the floor.")
    }
    else {
      msg("A red carpet covers the floor, worn almost though in places.")
    }    
  },
  scenery:true,
  isLocatedAt:function(loc) { return w[loc].room },
})

createItem('doors', {
  examine:function() {
    if (cloakHere() && player.isAtLoc('bar')) {
      msg("It is too dark to see the door properly.")
    }
    else {
      msg("All the doors are wooden, and painted white.")
    }    
  },
  scenery:true,
  isLocatedAt:function(loc) { return w[loc].room },
})