Custom Templates - ThePix/QuestJS GitHub Wiki

Quest 6 has a number of built-in templates (what in Quest 5 would be called types), but you may well want to add your own. How do you do that? And what could you do instead?

A template is actually just either a dictionary or a function that returns a dictionary. Using a function is best if there are options you want to set or if you are extending an existing type. Let us suppose you want a weapon type. You could do it as a dictionary:

const WEAPON = {
  weapon:true,
  damage:"1d4",
  bonus:0,
}

createItem("great_sword", TAKEABLE(), WEAPON,
  { damage: "2d6", }
);

Or as a function, extending the built-in TAKEABLE_DICTIONARY, and requiring the damage as a parameter:

const WEAPON = function(damage) {
  const res = Object.assign({}, TAKEABLE_DICTIONARY);
  res.weapon = true;
  res.damage = damage;
  res.bonus = 0;
  return res;
}

createItem("great_sword", WEAPON("2d6"),
  {  }
);

Note that both define a weapon attribute so we can later check if an item is of this type.

You need to be a little bit careful when copying a dictionary as is done with TAKEABLE_DICTIONARY. If the dictionary contains a list or another dictionary, a reference to that will get copied across, and any change made to one will (in effect) change them all, because behind the scenes there is only the one. If the dictionary you are copying contains only functions and simple values (numbers, string and Booleans) this is not a problem. If you need to include arrays or strings or arrays of numbers - and they will change during the game - you need to set it up as a function (as WEAPON is), rather than a dictionary. If you need other arrays or dictionaries, and they will change during the game, you need to do that, but will also need some custom code to get them to save, and really is better avoided.

afterCreation()

The afterCreation function will be run when an item is created. It has two special uses that are required to allow templates to work properly; verbs and name modifiers. The problem is that one template may want to add one set of verbs whilst another wants to add its own, and we need a way to let them both do so. Suppose we have a helmet with a light on it. The WEARABLE template will want to add "wear" or "remove" to the verbs, and "worn" to the name, while SWITCHABLE will want to add "switch on" or "switch off" to the verbs, and perhaps "providing light" to the name.

The solution is to give each item a list of functions, one for each template, and pass the list to each, giving each the opportunity to add to the list if it sees fit. And the way to add a function to the list is though the afterCreation() function.

This example is from TAKEABLE, and only handles the verbs; there is no name modifier. Note that you cannot use this.

  afterCreation:function(o) {
    o.verbFunctions.push(function(o, list) {
      list.push(o.isAtLoc(game.player.name) ? "Drop" : "Take")
    })
  },

It is complicated because we are defining a function that is adding another function to a list, but the only line that you would need to change is the middle one.

Here is a more involved example, from the WEARABLE template. Note, however, that the first two and last two lines are identical.

  res.afterCreation = function(o) {
    o.verbFunctions.push(function(o, list) {
      if (!o.isAtLoc(game.player.name)) {
        list.push("Take")
      }
      else if (o.getWorn()) {
        if (!o.getWearRemoveBlocker(game.player, false)) list.push("Remove")
      }
      else {
        list.push("Drop")
        if (!o.getWearRemoveBlocker(game.player, true)) list.push("WearW)
      }
    })
  }

For WEARABLE, we also need a name modifier, so let us add that. It looks very similar, as again we are adding a function to a list, asnd again that function takes an object and a list as parameters.

  res.afterCreation = function(o) {
    o.verbFunctions.push(function(o, list) {
      if (!o.isAtLoc(game.player.name)) {
        list.push("Take")
      }
      else if (o.getWorn()) {
        if (!o.getWearRemoveBlocker(game.player, false)) list.push("Remove")
      }
      else {
        list.push("Drop")
        if (!o.getWearRemoveBlocker(game.player, true)) list.push("Wear")
      }
    })

    o.nameModifierFunctions.push(function(o, list) {
      if (o.worn && o.isAtLoc(game.player.name)) list.push("worn")
    })
  }

If you are "inheriting" from another template, note that only one afterCreation will get run. If the original has one and you add one to your own template, only the later will be run. The way around that is to add the original afterCreation to the dictionary with another name, and have your afterCreation call that.

Here is an example from the RPG library, which inherits fromthe standard NPC.

const RPG_NPC = function(female) {
  const res = NPC(female);

  // ...
  
  res.oldRpgOnCreation = res.afterCreation
  res.afterCreation = function(o) {
    o.oldRpgOnCreation(o)
    o.verbFunctions.push(function(o, verbList) {
      verbList.push(lang.verbs.attack)
    })
    o.nameModifierFunctions.push(function(o, list) {
      if (o.dead) list.push(lang.invModifiers.dead)
    })
  }
  return res;
}

Note that in the WEARABLE example above, this deliberately replaced the original afterCreation in TAKEABLE.

Icons

To give items that use your template an icon, give it an "icon" function. The simple version just returns the name of the icon, but the second example shows how it can change depending on state.

  res.icon = () => 'key12'

  res.icon = function() {
    return this.closed ? 'closed12' : 'opened12'
  }

The icon itself should go into the icons folder. You need two, a light version, prefixed "l_" and a dark version, prefixed "d_". Thus, for the key the files will be "d_key12.png" and "l_key12.png". Icons should be 12 pixels square.

Alternatives

There are a number of alternatives to creating your own custom templates, and might be worth considering.

In a loop

If you are creating a set of similar items or locations, you could do them in a loop.

This example will create five identical locations (you would need to also create "boring_road_0" and "boring_road_6" at each end

for (let i = 1; i < 6; i++) {
  createItem("boring_road_" + i, {
    desc:'A log boring road going from east to west.',
    east:new Exit("boring_road_" + (i+1)),
    west:new Exit("boring_road_" + (i-1)),
  })
}

There is an example with items here.

Update an existing template item-by-item

Most templates set a flag saying what they are, so in the settings.setup function you could go through all the objects in the world, and modify these specific objects.

As an example, we will modify the TOPIC template. Let us suppose the user can select an attitude for how the player will interactive with men and another for women. This is set up here. Now in your topics, you want different scripts to run depending on that attitude. The way to do that is to give each TOPIC objec a modified "runscript".

the script could be set up elsewhere, perhaps in code.js. Here is an example. It assumes every object has either a "script" function or a full set of attitude scripts, "script_eager", "script_aloof", etc. (unlike the original, there is checking and you cannot use "msg").

const altTopicRunscript = function() {
  let obj = player.conversingWithNpc
  if (obj === undefined) return errormsg("No conversing NPC called " + player.conversingWithNpc + " found.")
  obj.pause()
  this.hideTopic = this.hideAfter
  const params = {char:obj, player:player, topic:this}

  if (this.script) {
    this.script.bind(obj)(params)
  }
  if (obj === w.Karina) {
    if (obj['script_' + player.attitude_to_Karina]) obj['script_' + player.attitude_to_Karina].bind(obj)(params)  
  }
  else if (obj.isFemale) {
    if (obj['script_' + player.attitude_to_Women]) obj['script_' + player.attitude_to_Women].bind(obj)(params)  
  }
  else if (obj.isMale) {
    if (obj['script_' + player.attitude_to_Men]) obj['script_' + player.attitude_to_Men].bind(obj)(params)  
  }
  this.showHideList(this.nowShow, true)
  this.showHideList(this.nowHide, false)
  this.count++
  world.endTurn(world.SUCCESS)
}

In settings.setup you would need to include this code:

  for (const key in w) {
    if (w[key].conversationTopic) w[key].runscript = altTopicRunscript
  }

Custom function

Another approach is to have your own create function that calls the base function, but then does extra stuff.

const createDungeonRoom = function(name, data) {
  const room = createRoom(name, data)
  room.afterEnter = function() {
    doMonsterStuff()
  }
  room.underground = true
}

If you want to allow templates, it gets more complicated, but can still be done. You could also do this for items in sets, as this simplistic example illustrates.

const createCupAndSaucer = function(desc, loc) {
  createItem("cup_" + desc, {examine:'A ' + desc + ' cup.', loc:loc})
  createItem("saucer_" + desc, {examine:'A ' + desc + ' saucer.', loc:loc})
}

Custom function and loop

You could combine the two above techniques. Here is a custom function that creates one room with an exit to the north, then goes through a loop to add a further six rooms to it.

function create_floor(name, north) {
  const dirs = ['northeast', 'east', 'southeast', 'southwest', 'west', 'northwest']  

  // create the hub, with exit to north
  const corridor = createRoom(name, {
    north:new Exit(north)
  })

  // for each create an exit to the room, then the room,
  // which is flagged as a hotel room, then the exit back
  for (const dir of dirs) {
    const roomName = name + '_' + dir
    corridor[dir] = new Exit(roomName)
    log(corridor)
    const room = createRoom(roomName, {
      hotelRoom:true,
    })
    const revDir = lang.exit_list.find(el => el.name === dir).opp
    room[revDir] = new Exit(name)
  }
}

Faking it

It might also be worth considering if you need multiple objects - could you fake it with just one? If you have the same bed in the six hotel rooms we created previously, one way to handle it would be to have one bed that has an "isAtLoc" function that returns true if the current room is flagged as a hotel room.

  isAtLoc:function(loc, situation) {
    if (typeof loc !== "string") loc = loc.name
    return w[loc].hotelRoom && situation === world.PARSER; 
  },

This does get more complicated if the item can change state. For a wardrobe, which can be open or closed, and have stuff inside it, this is not going to work.

Notes

Be thoughtful how you do this. It is easy to create a hundred rooms, but if they are a hundred identical hotel rooms with a hundred identical beds, you game is going to be pretty dull.

You might be tempted to randomly add descriptions and items. That would require some care, as the room will be different each time the player starts the game, and that will be a problem if the player then loads a saved game - everything will stay in the new configuration. One way around that might be to use the randome.prime facility, though that would take some care. A better way might be to pass a dictionary of options to your function, and have that control the room creation process, or better still, modify the objects after they are created.

create_floor('floor_4', 'lift')
w.floor_4.desc = 'The corridor smells of damp.'
w.floor_4_east.desc = 'This room is a mess!'