RPG Library: Quests - ThePix/QuestJS GitHub Wiki

NOTE: This is still a little experimental state, and may be subject to change, but it is getting there!.

A quest is a task that the player is given within a game, typically involving a number of steps to complete.

In any game the player clearly has something to achieve to win the game, but there may be parts to that, or besides that, that have a defined set of steps to reach a specific goal. This is different to a puzzle because the player is told what is necessary for the next step - though that could require solving a puzzle. Quests are often given by other characters, and might include retrieving a certain object, handing a certain object to a specific individual, or whatever.

This system is designed for games where the player might have several quests on the go at once; it is a way for both the user and the author to track progress for each. As such it seems to fit best with RPG games so is included in that library.

Authors create a quest using new Quest. This adds the quest to an array quest.data. Note that this is not saved when the player saves; this means a quest can be modified when the author updates a game. However, progress in a quest is attached to the player object, so is saved. If your game allows the player to control multiple characters, each character's progress in a quest is stored separately, so two or more characters can do the same quest, but each has to do every step of it.

Using Quests

If you are using the RPG library

You just need to make sure the "quest" file is included in the list.

settings.customLibraries.push({folder:'rpg', files:[
  "lang-en", "rpg", "skill", "attack", "item_templates", "npc_templates",
  "commands", "weapons", "monsters", "agenda", "spells", "quest",
]})

If you are not using the RPG library

The best solution is to copy the "quest.js" from the "rpg" folder to your game folder, and then add this to settings.js to have it included.

settings.files.push('quest')

You will need to ensure the file that defines your quests gets loaded after that one, so I suggest having a dedicated file, say called "quests.js" where the quests are defined, and have it load later, so now it will look like this:

settings.files.push('quest')
settings.files.push('quests')

Alternatively, you could include just this one file from the RPG library; however I think there is a significant risk of forgetting to include the file when you come to package the game ready for publication.

Creating

Use new Quest to create a quest; it requires a string that is the name of the quest, and a dictionary of data, just like createRoom and createItem. The dictionary should include an array called "stages" that is each stage of the quest.

new Quest('A carrot for Lara', {
  stages:[
    {text:'Go find a carrot.'},
    {text:'Give the carrot to Lara.'},
  ],
})

Handling

Once you have a quest created, you can use quest.get to retrieve it.

const q = quest.get('A carrot for Lara')

Now you have the Quest object, there is a suite of functions you can call on it. They all take the character as a parameter, but this is optional, and if omitted it will assume the player, which is going to be the case pretty much every time.

q.start(char, restart)  // Start the quest (fails if already started, unless `restart` is true)
q.complete(char)        // Complete the quest (fails if not active)
q.fail(char)            // Fail the quest (fails if not active)
q.moot(char)            // Moot the quest, i.e., it has become irrelevant (fails if not active)
q.next(char, label)     // Move the quest on to the labelled stage, or the next step if label it omitted (fails if not active)
                        // The user will be informed, and the text and script for the new stage will be used
q.jump(char, label)     // Jump the quest on to the labelled stage (the label parameter is required)
                        // Unlike next, the text and script will not be used; it only sets the current progress
q.state(char)           // Gets the state, an integer, for this quest
q.stage(char)           // Gets the current stage, a dictionary, for this quest if it is active, or `false` otherwise

Comments about quests use questmsg, rather than msg. This adds the "quest" CSS class, allowing you to differentiate the text. You should therefore also use questmsg inside quest functions.

Commands

The user can do QUESTS or Q to see a list of current quests, or QUESTS ALL or Q ALL for a list of all started quests.

Quests in action

To get quests to work, you need a mechanism that will get them from one stage to the next. There are two approaches to that, and we can call them internal and external. You can mix and match these even within the same quest.

External progression

External progression is a bit more flexible, and perhaps more intuitive at first glance. It simply means we set up other things in the game world to trigger the quest to move on.

Starting a quest is just a case of getting the Quest object, and calling the "start" function. In this example, a quest is started when the player first talks to Lara the rabbit.

  talk:function() {
    switch (this.talkto_count) {
      case 0 : 
        msg("You say 'Hello,' to the rabbit, 'how is it going?'")
        msg("The rabbit looks at you. 'Need carrots.' She looks plaintively at her round tummy. 'Fading away bunny!")
        quest.get('A carrot for Lara').start()
        break
      default: msg("You wonder what you can talk to the rabbit about."); break
    }
    return true
  },  

Note that it is safe to do this repeatedly for the same quest, as later calls will have no effect.

We can then have the carrot item progress the quest. Note that for the second step, we use the label. If the first step is missed somehow, giving the carrot will always end the quest (admittedly in this case you need to give the carrot, so if there was another way for Lara to get the carrot you would need to include code there for progressing the quest).

new Quest('A carrot for Lara', {
  stages:[
    {
      text:'Go find a carrot.',
      intro:'The rabbit claims she will fade away unless given a carrot.',
    },
    {
      text:'Give the carrot to Lara.', 
      intro:'Now you have the carrot you better give it to Lara quickly before she fades away.',
      label:'carrot found',
    },
  ],
})

We also need to set up the carrot to react to being moved, and have it progress the quest.

createItem("carrot", TAKEABLE(), {
  loc:"practice_room",
  examine:"A large carrot.",
  afterMove:function(options) {
    if (options.toLoc === player.name) {
      quest.get('A carrot for Lara').next('carrot found')
    }
    if (options.toLoc === 'rabbit') {
      quest.get('A carrot for Lara').complete()
    }
  },
})

Note that the label points to the stage the quest should go to. You can jump back to earlier stages.

Note also that we may need a mechanism to stop the carrot being taken too earlier. If it is taken before the quest starts, the second stage may never happened. Alternatively we can modify the quest, so that first stage will jump ahead if the player already has the carrot.

new Quest('A carrot for Lara', {
  stages:[
    {
      text:'Go find a carrot.',
      script:function(params) {
        questmsg('The rabbit claims she will fade away unless given a carrot.')
        if (w.carrot.loc === player.name) {
          questmsg('Luckily you already have one - you can give her that.')
          params.quest.jump(player, 'carrot found')
        }
      }
    },
    {
      text:'Give the carrot to Lara.', 
      intro:'Now you have the carrot you better give it to Lara quickly before she fades away.',
      label:'carrot found',
    },
  ],
})

Internal progression

With internal progression, the quest decides for itself when to start and when to move to the next stage. I prefer it because every for the quest is in the one place.

Each stage needs a "test" attribute; the quest moves to the next stage when the test returns true. You also need a "startTest" attribute to kick start it all.

new Quest('A carrot for Lara', {
  startTest:function() { return w.rabbit.talkto_count > 0 },
  stages:[
    {
      text:'Go find a carrot.',
      test:function(chr) { return w.carrot.loc === chr.name},
    },
    {
      text:'Give the carrot to Lara.', 
      test:function(chr) { return w.carrot.loc === 'rabbit'},
    },
  ],
})

After the player first talks to the rabbit, "startTest" will return true and the quest become active. Once it is active, at the end of each turn the "test" attribute of each stage will be checked to see if that stage has been done.

When the quest is given, the player is looking for a carrot. At the end of each turn the first "test" attribute will be run. It will return true if the player currently has the carrot. The quest will then move on to the second stage.

Once the quest has been progressed, the system will stop testing stages that have been completed. Also note that stages are tested from the bottom up, so if the condition for a later stage is met earlier, it will jump straight there.

While this works great for doing things out of order normally, it is not so good for the first step, as this will always happen first, and it will not progress to the right stage until the next turn. In the example, if the player picks up the carrot first, the quest will say she needs to get a carrot after talking to the rabbit, and then on the next turn will note she already has the carrot and will move the quest on. However, we can use the same trick here that we did for external progress (note that we need to add a "label" here; we already had that before).

new Quest('A carrot for Lara', {
  startTest:function() { log('here');return w.rabbit.talkto_count > 0 },
  stages:[
    {
      text:'Go find a carrot.',
      script:function(params) {
        questmsg('The rabbit claims she will fade away unless given a carrot.')
        if (w.carrot.loc === player.name) {
          questmsg('Luckily you already have one - you can give her that.')
          params.quest.jump(player, 'carrot found')
        }
      },
      test:function(chr) { return w.carrot.loc === chr.name},
    },
    {
      text:'Give the carrot to Lara.', 
      intro:'Now you have the carrot you better give it to Lara quickly before she fades away.',
      test:function(chr) { return w.carrot.loc === 'rabbit'},
      label:'carrot found',
    },
  ],
})

Note 1: It will only test this for the player; if other characters have quests too, no checking is done for them.

Note 2: The test for a particular stage should return true when the stage is completed, not when it becomes active. In the example, the first stage is to get a carrot; the "test" function returns true when the player has done that. This is unlike external progression, where you would use the label for the new stage.

Note 3: There is no going back. If the player subsequently loses the carrot, the quest will not change; it will still think the carrot has been obtained. Some options to consider: use external progress to move it back; set up the carrot so it cannot be dropped; or just accept it how it is.

Attributes for Stages

All are optional except "text".

Attribute Type comments
text string A summary of what this stage requires the player to do.
test function that returns a Boolean At the end of each turn, this will be run (if the quest is active and currently at this or an earlier state). If it returns true, the quest will progress to the next stage.
label string External progression can use this label to identify this stage in the "next" function.
script function Runs when this stage starts. Be mindful of the possibility of it running more than once or not at all if that is possible for your quest.
intro string Printed when this stage starts. Details what the player is to do.

When a quest is progressed, the system will first say "Quest progress" or similar with the name of the quest, followed by the "text" attribute for this stage, then "script" will be run with any output appearing, and finally the "intro" text will be printed.

The "script" function will be passed a dictionary with "char" set to the character, "oldStage" set to the previous stage, "oldStageIndex" set to its number (they count from zero), and "quest" set to the quest. Both "oldStage" and "oldStageIndex" will be undefined if there was no previous stage - the quest is being started or restarted.

Language support

Because it is not part of the normal libraries, you will need to set the language strings up yourself. This would need to go into code..js or quests.js, with the strings translated into your language.

quest.stateNames = ['', 'Active', 'Moot', 'Failed', 'Success']
quest.stateComments = ['Quest Started', 'Quest Progress', 'Quest Moot', 'Quest Failed', 'Quest Completed']