A Random Dungeon - ThePix/QuestJS GitHub Wiki

Creating a Whole Dungeon

So you want to create a random dungeon. This will involve spawning rooms, treasures, monsters, etc., and it will be useful to read creating objects on the fly first. That said, somethings are best not cloned. You just need a single instance of all the stuff the player cannot pick up and tell Quest to place it in the right rooms (whether the room itself is a clone or not).

Rooms

One of the major problems with creating random rooms is having the items in the room match the description. If the description says there are magical glyphs on the walls, we want the player to be able to examine them. There is therefore more to this than just creating random descriptions; we need to generate both items and descriptions together.

As this is a tutorial, we will do this in a fancy way so you can see a useful technique. The reason to do this is that we are likely to have a lot of these items, and they are all pretty similar. So we set up lists of them that defines the differences:

prototypeItems = [
  { alias:'bone', type:'dungeon', t:'Just a bone, probably {random:human:orc:goblin:elf}. Junk.', tpNow:true},
  { alias:'rusty sword', type:'dungeon', template:WEAPON('d4'), t:'An old short sweord; not ay use as a weapon now.', desc:'A rusty sword lies in the corner.'},
  { alias:'ring', type:'dungeon', price:'d10 + 10', t:'Just a cheap ring, worth maybe {money:item:price}.'},
]

Now we need to go through the list and create each item. This is where all the fancy stuff is done, so this will get complicated. That said, you can just copy-and-paste much of it.

To keep stuff together, I am putting it all inside a dictionary called "dungeon". So far it just has three things in it, two functions and array.

The array, "clutter", will be important later - we will be filling it with objects that can be randomly added to rooms, and to facilitate that objects will be given an "apply" function that will add them (or a clone) to that room.

The "createDungeonItem" function creates a single item, based on values in the given dictionary (which is not the same as the normal dictionary used when creating an item). The "createDungeonStuff" function iterates through three lists, and uses "createDungeonItem" to create each item, having first added a few extra values to the data, depending on the type of item.

const dungeon = {
  clutter:[],

  createDungeonItem:function(data, addToClutter) {
    let name = "r_" +  verbify(data.alias)
    if (data.type) name += '_' + data.type
    if (data.flag) name += '_' + data.flag
    name = util.findUniqueName(name)

    const template = data.template ? data.template : TAKEABLE()
    
    const merch_template = data.price ? MERCH(data.price) : {}
    
    const item = createItem(name, template, merch_template, {
      scenery:data.scenery,
      flag:data.flag,
      room_type:data.type,
      room_flag:data.flag,
      func:data.f,
      alias:data.alias,
      apply:data.apply,
      desc:data.desc,
      synonyms:data.synonyms,
      text:data.t,
      tpNow:data.tpNow,
      
      examine:function(options) {
        let s = this.text
        if (currentLocation && currentLocation[this.alias + 'Addendum']) s += ' ' + currentLocation[this.alias + 'Addendum']
        msg(s, options)
      }  
    })
    if (data.isLocatedAt) item.isLocatedAt = data.isLocatedAt
    if (data.data) {
      for (const key in data.data) item[key] = data.data[key]
    }
    if (data.apply) dungeon.clutter.push(item)
  },


  createDungeonStuff:function() {
    for (const data of dungeon.backdropItems) {
      data.template = {}
      data.scenery = true
      data.isLocatedAt = function(loc, situation) { return w[loc].room_type === this.room_type }
      const item = dungeon.createDungeonItem(data)
    }

    for (const data of dungeon.roomDressingItems) {
      data.template = {}
      data.scenery = true
      data.isLocatedAt = function(loc, situation) { return w[loc]['r_' + this.room_flag] }
      data.apply = function(room) {
        room['r_' + this.flag] = true
        if (this.func) this.func(room)
        if (this.desc) room.desc += ' ' + this.desc
      }

      const item = dungeon.createDungeonItem(data)
    }

    for (const data of dungeon.prototypeItems) {
      data.template = data.template ? data.template : TAKEABLE()
      data.scenery = data.desc ? true : false
      data.apply = function(room) {
        const obj = cloneObject(this, room.name)
        if (typeof obj.price === 'string') obj.price = random.dice(obj.price)
        if (this.desc) room.desc += '{if:' + obj.name + ':scenery: ' + this.desc + '}'
        if (this.tpNow) {
          obj.text = processText(obj.text)
        }
      }

      const item = dungeon.createDungeonItem(data)
    }
  },
}

So now we need three lists of items.

The first list is for items that are everywhere, but cannot be picked up - walls, floors, etc. These are assumed to vary, depending on the nature of the room, and this is defined by the "room_type" attribute of the location. The examples below will only be present if the "room_type" is set to "dungeon"; you might want to have different entries with "room_type" set to "ice cave", "crypt", etc.

/*
alias    The alias, as normal. The name will be generated from this
type     The room type this item is found in
t        The description to use for EXAMINE
data     Optional. A dictionary of other values to be added to the item
*/
dungeon.backdropItems = [
  { alias:'walls', type:'dungeon', t:'The walls are stone blocks, roughly cut.'},
  { alias:'floor', type:'dungeon', t:'The floor is hard-packed earth.', synonyms:['ground']},
  { alias:'ceiling', type:'dungeon', t:'The walls arch up to form a vaulted ceiling above you.'},
]

The second list is for scenery - items that are present in occasional rooms, but cannot be picked up. These need to be mentioned in the room description, and you can do that either by giving a "desc" attribute - this will simply be appended - or an "f" function.

The function will be passed the room, and you can do what you want. See the glyphs example; the description for the walls will have a note added. You an do this for any item from the first list, just use the alias with "Addendum" appended as the room attribute name.

/*
alias    The alias, as normal. The name will be generated from this
flag     Any room with this flag (prepended with "r_" will have this item present
type     Optional. The room type this item might be found in
t        The description to use for EXAMINE
f        Optional. If present, when the apply function is used, this will be called, and passed the room object.
desc     Optional. If present, this will be added to the room description.
data     Optional. A dictionary of other values to be added to the item
*/
dungeon.roomDressingItems = [
  {
    alias:'slime', flag:'slime',
    t:'The slime is clear, but tinted green.',
    desc:'There is slime running down the walls.',
  },
  {
    alias:'glyphs', flag:'glyphs',
    t:'The glyphs are written in blood....',
    f:function(room) {
      room.desc += ' There are arcane glyphs written on the walls.'
      room.wallsAddendum = ' There are arcane glyphs written on them.'
    },
    data:{read:'You try to read the glyphs, but it just causes you to feel dizzy. Perhaps they are not meant for mortal minds...'},
  },
]

The third list is items that can be picked up.

/*
alias    The alias, as normal. The name will be generated from this
type     The room type this item might be found in
t        The description to use for EXAMINE
tpNow    Optional. If true, the text processor will be run when this item is created. This can be useful for giving variety to clones.
desc     Optional. If present, this item will be scenery and this text will be added to the room description. 
         It will be inserted into a text processor directive that will handle when the text should be seen.
data     Optional. A dictionary of other values to be added to the item
price    Optional. The value of the item. Note that this should be a string, and you can use stand RPG dice conventions to add variety (eg "2d6+5")
*/
dungeon.prototypeItems = [
  { alias:'bone', type:'dungeon', t:'Just a bone, probably {random:human:orc:goblin:elf}. Junk.', tpNow:true},
  { alias:'rusty sword', type:'dungeon', template:WEAPON('d4'), t:'An old short sword; not much use as a weapon now.', desc:'A rusty sword lies in the corner.'},
  { alias:'ring', type:'dungeon', price:'d10 + 10', t:'Just a cheap ring, worth maybe {money:item:price}.'},
  
]

We just need to call "createDungeonStuff" to get the lists converted to actual items.

dungeon.createDungeonStuff()

Now we need to see if it works. This code needs to go into settings.setup() (or better, a function called by it), as Quest will throw an error if you try to clone something before that.

It adds an exit to the north of a room called "practice_room", and then generates five rooms heading north.

Each room is given something to test the features we just added. This is done by taking an item from the clutter array, and calling its "apply" function.

    const number_of_clone_rooms = 5

    w.practice_room.north = new Exit('clone_room1', {dir:'north', origin:w.practice_room})
    endTurnUI(true)


    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 = dungeon.dungeonAdjectives[i] + ' ' + dungeon.dungeonNouns[i]
      clone.headingAlias = (clone.alias.match(/^[aeiou]/) ? 'An ' : 'A ') + clone.alias
      clone.desc = 'A dungeon room.')
      clone.room_type = 'dungeon'
      if (i <= dungeon.clutter.length) dungeon.clutter[i - 1].apply(clone)
    }