Handling events (and turnscripts) - ThePix/QuestJS GitHub Wiki

QuestJS offers a number of options for tracking time and scheduling events.

Turnscripts

If you are used to Quest 5, Quest 6 does not have turnscripts as such. Instead you can use any object as a turnscript by giving it the right attributes.

If you have an object that has to do something every turn, you can make that object itself be its own turnscript. A great example would be tracking the battery of a torch; you can attach the script that does that to the torch itself, rather than a turnscript in another part of the game. If you clone an object with a turnscript, the clone will have its own turnscript, and they will run entirely separately.

The system has a few extra options that makes it rather more flexible, but we will start with a simple example. This is not attached to another object, so in effect is just like a Quest 5 turnscript.

createItem("event0", 
  {
    eventPeriod:1,
    eventActive:true,
    eventScript:function() { msg("turnscript") }
  }
)

The "eventScript" is what we want to happen every turn. We set "eventActive" to true to have it running, and "eventPeriod" to 1 to have it happen every turn.

Quest uses another attrbute, "eventCountdown" to track where it is in the event period. Say we set eventPeriod to 5; after the event fires, "eventCountdown" will get set to 5, and then each turn after that the countdown is reduced, until we get to zero and the event fires again.

In the second example, "eventPeriod" is not set, so this will only fire once. However, we are setting "eventCountdown" to 3, so it will then wait three turns before firing

Also, "eventActive" is not set, so at the start of the game it does nothing. However, we could set it to true (w.event1.eventActive = true) when the player does something; it will then wait three turns before firing the script.

createItem("event1", 
  {
    eventCountdown:3,
    eventScript:function() { msg("test1 happened: " + this.name) }
  }
)

The third example will wait seven turns before firing, but will then fire again every five turns after that.

createItem("event2", 
  {
    eventCountdown:7,
    eventPeriod:5,
    eventActive:true,
    eventScript:function() { msg("test2 happened: " + this.name) }
  }
)

In the next example, there is an "eventCondition" attribute. The event will only fire when at least twelve turns have passed and the player is in the given room. If the player is not in the room, the event will fire the next time she is. This is useful if you want to make sure the player witnesses the event.

createItem("event3", 
  {
    eventCountdown:12,
    eventActive:true,
    eventCondition:function() { return player.loc === "hallway" },
    eventScript:function() { msg("test3 happened: " + this.name) }
  }
);

Just for completeness, in the next example, the event fire after twelve turns, if the player is in the room. If the player is not there at that moment, it will never fire.

createItem("event4", 
  {
    eventCountdown:12,
    eventActive:true,
    eventScript:function() {
      if (player.loc === "hallway") {
        msg("test3 happened: " + this.name)
      }
    }
  }
)

The default "eventIsActive" attribute is a function that just returns the value of "eventActive", but you can override, for example to use a different attribute. Here is how you might set up the torch mentioned earlier (just the relevant attributes shown). Now the event only fires when the torch is switched on, so the battery only deteriorates when it is being used.

    eventPeriod:1,
    eventIsActive:function() {
      return this.switchedon;
    },
    eventScript:function() {
      this.power--;
      if (this.power === 2) {
        msg("The torch flickers.");
      }
      if (this.power < 0) {
        msg("The torch flickers and dies.{once: Perhaps there is a charger in the garage?}");
        this.doSwitchoff();
      }
    },

Scheduling Events

What if you have things happening to a timetable? You might be writing a detective story, and the murderer is still active; the player has to act to stop more people being murdered. There are, therefore, events scheduled to fire at various points throughout the game. What is the best way to do that?

If this is a simple timetable with events occurring on specific turns, regardless of what the player has done, we can set up a simple event script that fires the right event.

You could do this with any object, but I am going to use a dedicated object called "timetable". There is an "eventCounter" that tracks the current turn, and a dictionary of events. If the value of "eventCounter" is in the dictionary, that function will run.

createItem("timetable", {
  eventPeriod:1,
  eventActive:true,
  eventCounter:0,
  eventScript:function() {
    this.eventCounter++
    if (this.events[this.eventCounter]) this.events[this.eventCounter]()
  },
  events:{
    3:function() {
      msg("Three turns have passed!")
    },
    7:function() {
      msg("Seven turns have passed already, and - seriously - what have you achieved?")
    },
  },
})

Note that we are using numbers as the keys for the dictionary. JavaScript is quite flexible about that. Generally it is best to use the same rules as variables (strings with no special characters or spaces), but occasionally it is useful to do otherwise.

Although this is described as a strict timetable, there is some flexibility here. You can pause the count at any time by setting w.timetable.eventActive to false, and have it resume later. Your events can also behave differently depending on the current situation.

Agendas

Alternatively, we can tap into the agenda system for NPCs. We do not need a full NPC, just an AGENDA_FOLLOWER, set up like this:

createItem("timetable", AGENDA_FOLLOWER(), {
  counter:0,
  inSight:function() { return true },
  agenda:[
    'wait',                            // wait one turn
    'run:script',                      // run the script "script" on this object
    'wait:2',                          // wait two turns
    'run:script:2',                    // run the script "script" on this object with the parameter "2"
    'waitFor:checkStuff',              // wait until the "check" script returns true
    'run:otherScript'                  // run the script "otherScript" on this object
    'wait:3',                          // wait three turns
    'waitFor:checkStuff:otherScript'   // wait until the "check" script returns true, and immediate run "otherScript"
  ],
  script:function(n) {
    this.counter += (n[0] ? parseInt(n[0]) : 1) 
  },
  otherScript:function(n) {
    msg("Counter is " + this.counter) 
  },
  checkStuff:function() { return this.flag },
})

The important attribute is "agenda". This is an array of steps.

Each instruction consists of a function name, followed by a number of parameters, all separated by colons. In the example above, there is a comment as well. The scripts here are trivial, but could potentially do anything; move NPCs around, change the weather, start the apocalyse...

You can set the "suspended" attribute to true to temporarily halt an agenda. Set back to false to get it going again.

NPC agendas have several options for instructions, but most are not really applicable here, and some will just not work. Those in the example above will probably do all you need, given the functions can do anything. In fact, as a general strategy, I would suggest you use "wait" to delay for a set time, then "waitFor" to ensure conditions are right, and use that to call the function. Repeat as often as you need.

The "inSight" function will make Quest show the messages; it will think this is an NPC that is present in the room, so his actions should be apparent.

Branching

You can change the schedule at any time with setAgenda. The old agenda will be forgotten, and the new one started (suspended will be set to false whatever its previous state).

This allows you to change the scheduled events depending on what the player did. In this rather bland example, the "doStuffTwo" function sets a new agenda, depending on the state of the flag.

createItem("timetable", AGENDA_FOLLOWER(), {
  counter:0,
  agenda:[
    'waitFor:checkStuffOne:doStuffOne'
    'waitFor:checkStuffTwo:doStuffTwo'
  ],
  doStuffOne:function(n) {
    // ...
  },
  doStuffTwo:function(n) {
    if (this.flagThree) {
      this.setAgenda([
        'waitFor:checkStuffThree:doStuffThree'
        'waitFor:checkStuffFour:doStuffFour'
      ])
    }
    else {
      this.setAgenda([
        'waitFor:checkStuffFive:doStuffFive'
        'waitFor:checkStuffSix:doStuffSix'
      ])
    }
  },
  checkStuffOne:function() { return this.flagOne },
  checkStuffTwo:function() { return this.flagTwo },
  checkStuffOne:function() { return this.flagOne },
  // ...
})

Coordinated events with "respond"

The schedule is a great way to manage the high level events, but what if you need finer control? By that I mean that the schedule can be used to determine what phase we are in the game, is it dinner time, has the party started? But sometimes you need to orchestrate events on a turn by turn basis.

A great way to do this is with the respond function. It is not trivial, but it will allow you to create NPCs that seem to live independent lives, reacting to what goes on around them. There is a discussion of using respond with a timetable at the end of that page.

A Staged approach

Another way to handle scheduling is to break the game into a sequence of stages. This is a more high level approach, and you might want to still use the above to handle the turn-by-turn details, while a stage might last several hours. For example, one stage could be the player going to the dining room. Once there, stage two kicks in, where food is served, according to an agenda. After dinner stage three starts.

This is probably best put inside the timetable object we created before. We need a few attributes. A function that handles it all, and a dictionary of functions to fire as we progress from one stage to the next, and an attribute to store the current value.

  stage:'beforeDinner',
  stagesElapsed:[],
  afterStage:function(name) {
    return this.stagesElapsed.includes(name) 
  },
  launchStage:function(name) {
    debugmsg("STAGE {show:name} IS HERE!", {name:name})
    this.stage = name
    this.gameStages[name]()
    for (let key in w) {
      if (w[key]['loc_' + name]) w[key].loc = w[key]['loc' + name]
      if (w[key]['agenda_' + name]) w[key].agenda = w[key]['agenda_' + name]
      if (w[key]['launchStage_' + name]) w[key]['launchStage_' + name]()
      if (w[key]['show_' + name]) w[key].show()
      if (w[key]['hide_' + name]) w[key].hide()
    } 
  },
  gameStages:{
    dinner:function() {
    },
    afterDinner:function() {
    },
  }

When dinner starts, do w.timetable.launchStage('dinner') to move to the next stage, and the "dinner" function will run.

In addition, and object with a "loc_dinner" attribute will be moved to that location, an "agenda_dinner" agenda will start that agenda, and a "launchStage_dinner" function will have that function called. Also, any topic with "show_dinner" will be shown and with "hide_dinner" will be hidden (following the usually rules for topics). This means you can set up your NPCs and topics to react to the change in stage, keeping the code there rather than in the timetable, which I think makes it easier to modify later.

If during play you want to know if this is the dinner stage, do:

  if (w.timetable.stage === 'dinner') {
    // ...

If you need to know if we are in a stage after the dinner stage (and not currently the dinner stage), use:

  if (w.timetable.afterStage('dinner')) {
    // ...

A Note on Order

You might need to know in what order things happen at the end of the turn. It is all done in world.endTurn().

  • Any change listeners are checked and run if a change occurred (this will happen even if the command failed)
  • Turn count in increment
  • The elapsed time is updated
  • The "endTurn" function attribute of each item is run in turn
  • The "endTurn" function attribute of any module listed in settings.modulesToEndTurn is run in turn
  • Change listeners are again checked and run
  • NPC pauses are reset
  • Item scope, including darkness, is evaulated
  • Game state is saved for UNDO handling
  • The UI is updated (this will partly happen even if the command failed)