Creating objects on the fly - ThePix/QuestJS GitHub Wiki

Background

Secure JavaScript (i.e., JavaScript that gives the least risk to the user) cannot convert a string to a script. An obvious result is that you cannot use the eval function, which is in both Quest 5 and JavaScript. That is not much of a big deal most of the time. However, what is a big problem is that if you save an object during play, the only way a script can be saved is as a string, and when that object is loaded into the game again, none of its scripts will work because they will remain as strings.

The solution to that is an entirely different approach to saving games, which is a little more restricted - you cannot have objects that change their scripts for example - but does allow an author to update her game and users to then load an old save game into the new version. This is discussed in more detail here.

What is important here is that the new save system does not allow new objects to be created.

So what are we to do if we want to create an item?

Cloning

While you cannot create a completely new item, you can create a clone of an existing one. A shop is a great example of when this is useful. Rather than have the player buy the only bottle of milk in the shop, clone the bottle of milk, and give that to the player.

What is the difference? Well, behind the scenes a clone gets its scripts by copying them from the prototype, so there is no security risk. For authors, the difference is that you need to have a prototype set up in advance.

To clone an object, use the cloneObject function. This takes three parameters, the prototype, the name of the location to put the clone in and a name for the prototype. In fact the location and name are optional; if no name is given a new one is generated based on the name of the prototype (but will be unique).

cloneObject(w.tamarind_pod_prototype, 'greenhouse_west')

The function returns the clone. This allows you to modify it after creation.

const clone = cloneObject(w.tamarind_pod_prototype, 'greenhouse_west')
clone.examine = processText('The pod is {random:pale brown:brown:tan:beige}, knobbly and about {random:an inch:an inch and a half:two inches} long.')

Note that the text directives are handled at creation, so they are set in stone for this clone. If it was done when the text is displayed, it would change each time.

Clones are not quite exact copies; these attributes will be different:

  • name: Each clone will have a unique name
  • clonePrototype: Set to the prototype (if you clone a clone, this will be set to the original, not the cloned clone)
  • loc: Set to the location specified when the clone was created (undefined if none was set)
  • getSaveString: This attribute is used when saving the game, and is different for clones

NOTE: You cannot clone an object before the object is initialised - if you try, you will get a error. Object initialisation occurs after all objects are created, and code in your files are run, but before settings.setup() is run, so if you want to clone something before the game starts, do in in settings.setup().

Modifying Clones

You can change clones quite a bit, as long as you only change string or number attributes. Here is an example of a function that will create a drink from a generic prototype, but then change its attributes to allow for different drinks.

const createDrink = function(data) {
  const drink = cloneObject(w.drink_prototype)
  drink.setAlias(data.alias)
  drink.alcohol = data.alcohol
  drink.examine = data.examine
  drink.msgDrink = data.drink
  drink.loc = player.name
  return drink
}

The prototype might look like this:

createItem("drink_prototype", TAKEABLE(), {
  drink:true,
  heldVerbs:["Sip", "Drink"], 
  drink:function(options) {
    msg(this.msgDrink, options)
    options.drunk += this.alcohol
    delete w[this.name]
  },
})

The data for each drink is a dictionary, which could be in an array somewhere, perhaps on the bar tender:

  drinkList:[
    {
      name:'red wine', 
      alias:'glass of red wine', 
      examine:'A wineglass half full of red wine', 
      drink:'You gulp back the red wine.',
      alcohol:1,
    },
    {
      name:'vodka', 
      alias:'glass of Devlune vodka', 
      examine:'A glass tumbler with an inch of vodka in it.', 
      drink:'You take a breath, then knock the vodka back in one.',
      alcohol:3,
    },
  ],

If you are using dynamic conversations, you could then use that list to generate some conversation topics.

for (const d of w.barman.drinkList) {
  createItem("barman_" + verbify(d.name), TOPIC(true), {
    loc:'barman',
    alias:"Get me a " + d.name,
    hideAfter:false,
    data:d,
    script:function() {
      let s = "'Get me a {show:drink:name},' you say to the barman.|"
      s += "'Certainly.' He pours a generous glass of {show:drink:name}, and slides it over the bar to you."
      msg(s, {drink:options.topic.data})
      createDrink(options.topic.data)
    }
  })
}

What I like about systems like this is that if I later want to add a new drink to the list, it is just an addition to the array, "drinkList". Everything else will be done for you.

Modifying List (Array) and Dictionary Attributes of Clones

This requires some care because of the way JavaScript handles these. It passes around references to them, rather than the thing itself. When you create a clone, the clone gets a reference to the list or dictionary - the same one as the original. This means that if you change the list or dictionary for one, you are (in effect) changing it for both.

The best strategy is to not change them! If you have to, ensure you create a copy first.

const clone = cloneObject(w.tamarind_pod_prototype, 'greenhouse_west')
clone.changingList = clone.changingList.slice()

Naming clones

By default clones are given the name of the prototype with a number appended to keep it unique, so for the above that would be drink_prototype0, drink_prototype1, etc. If your item has a name ending in numerals, Quest will handle that fine - though you may be confused!

You can specify a name by using a third parameter.

cloneObject(w.tamarind_pod_prototype, 'greenhouse_west', 'pod_clone')

If the name is not unique, numbers will again be appended.

Cloning rooms

You can clone rooms as well as items. However, you may have a problem with exits. If you just want the same exits, that is fine, but if you want to have new exits, exits to different locations, then you need to use the EXIT_FAKER template on your room prototype.

Your prototype might look like this:

createRoom('room_prototype', EXIT_FAKER(), {
  paintedOnDoor:function(char) {
    char.msg(lang.stop_posture(char))
    msg("You try the door to the " + this.dir + " but it is just painedt on!")
    return false
  },
})

It needs to include all the "simpleUse" functions for the cloned exits; in this example, I just have one, which I have called "paintedOnDoor".

You then need a way to generate rooms with appropriate exits. There is a lot to consider here, and that is for you to sort out. However, this simple example shows how to use QuestJS (and should be in settings.setup()). It just clones the prototype five times; the first room has an exit to a location called 'practice_room', and each location has an exit between its neighbours. The last room has a special exit that uses the "paintedOnDoor" function.

const number_of_clone_rooms = 5

for (let i = 1; i <= number_of_clone_rooms; i++) {
  const clone = cloneObject(w.room_prototype, undefined, 'clone_room' + i)
  if (i === 0) {
    clone.exit_south = 'practice_room'
  }
  else {
    clone.exit_south = 'clone_room' + (i - 1)
  }
  clone.exit_msg_south = 'Back to the practice room, you trot.'
    
  if (i < number_of_clone_rooms) {
    clone.exit_north = 'clone_room' + (i + 1)
    clone.exit_msg_north = 'Further and further from the practice room, you venture.'
  }
  else {
    clone.exit_north = '_'
    clone.exit_func_north = 'paintedOnDoor'
  }
  clone.alias = 'strange room'
  clone.headingAlias = 'A strange room'
  clone.desc = processText('A {random:small:large:strange:airless} {random:hallway:chamber:passage:corridor}, with {random:high windows:a vaulted ceiling:arcane glyphs on the walls:an odd smell:a disturbing statue}.')
}

Note that if you are using headings for locations, then you must set "headingAlias" for each clone, and you also need to do "alias" and "desc", as you see above. Ideally the aliases would reflect the descriptions, and any items mentioned in the descriptions will get implemented (by cloning of course) so the player can examine them.

Then there is how you decide how each location relates to the others...

Clones before the game starts

There are special considerations when cloning an object before the game starts, revolving around name modifiers and display verbs. The way to handle this is to simple use copyObject rather than cloneObject. They are used just the same.

const clone = copyObject(w.tamarind_pod_prototype, 'greenhouse_west')

A copy will not have a "prototype" attribute, and is saved as a normal object, rather than as a clone.

If you try to use the wrong function, Quest will tell you!