NPCs: Following an agenda - ThePix/QuestJS GitHub Wiki

You can give your NPC the illusion of autonomy by given him or her an agenda; a list of instructions to follow.

The agenda attribute is, then, an array of strings. Each string is an instruction, most taking one turn.

    agenda:[
      "text:Arthur stands up and stretches.", 
      "text:'I'm going to find Lara, and show her the garden,' says Arthur.|'Whatever!'", 
      "walkTo:Lara:'Hi, Lara,' says Arthur. 'Come look at the garden.'",
      "joinedBy:Lara:'Sure,' says Lara.",
      "walkTo:garden:'Look at all the beautiful flowers,' says Arthur.",
    ],

Each instruction consists of a function name, followed by a number of parameters, all separated by colons. In the example above, the first two use the text function, which just prints each of the following parameters - so the second will print two messages - as long as the player is in the same room as the NPC.

The third line uses the walkTo function, so the NPC will move to the destination one room per turn. In this case the parameter is another NPC, so the destination will be the room she is in. You can also use "player" or the name of the room. In the turn that the NPC arrives at the destination, the other parameters will be printed out (using text, so the same rules apply).

The fourth line has another NPC join this one. Hereafter they will act as a group (use disband to have them break up). Again, the other parameters get printed using text.

The last line again uses walkTo to have the two NPCs head to another destination.

Pause and suspend

You can stop an NPC using the pause function of the NPC. If the NPC is in a group, this will pause the group. The NPC (or group) will be paused for one turn only. The built-in commands will all pause an NPC if applicable (if the player talks to the NPC or tells the NPC to do something).

You can set the "suspended" attribute to true to temporarily halt an agenda. If the NPC is part of a group, you need to do this on the leader. The agenda will resume when you set the "suspended" attribute to false.

In this example, the NPC starts asleep ("suspended" set to true); talking to him will wake him up, at which point he will follow the agenda.

createItem("Arthur",
  NPC(false),
  { 
    loc:"garden",
    examine:function() {
      if (this.suspended) {
        msg("Arthur is asleep.")
      }
      else {
        msg("Arthur is awake.")
      }
    },
    suspended:true,
    properName:true,
    agenda:[
      "text:Arthur stands up and stretches.", 
      "text:'I'm going to find Lara, and show her the garden,' says Arthur.|'Whatever!'", 
      "walkTo:Lara:'Hi, Lara,' says Arthur. 'Come look at the garden.'",
      "joinedBy:Lara:'Sure,' says Lara.",
      "walkTo:garden:inTheGardenWithLara:'Look at all the beautiful flowers,' says Arthur.:Through the window you see Arthur say something to Lara.",
      "text:Lara smells the flowers.",
    ],
    inTheGardenWithLara:function(arr) {
      if (this.here()) {
        msg(arr[0])
      }
      if (game.player.loc === "dining_room") {
        msg(arr[1])
      }
    },
    talkto:function() {
      msg("'Hey, wake up,' you say to Arthur.")
      this.suspended = false
      this.pause()
      return true
    }
  }
);

Note that the "talkto" attribute sets "suspended" to false so the agenda will run, but it also pauses the NPC, so he does nothing this turn.

Also, the walkTo agenda item uses a function, "inTheGardenWithLara". The agenda system checks if the third value is a function of the NPC, and will run it, passing the remaining values to it as an array. In this case this is used to give a different message depending on whether the player is there in the garden or can see it through the window.

Agenda functions

msg: Simple prints the text to screen. The array is put back together as a single string; you can use a vertical bar, |, to break the text into multiple paragraphs

text: If the first parameter is the name of a function attribute of the NPC, that function is run, sent the other parameters. It should return true to move on to the next step, or false to continue this step.

If the first parameter is not the name of a function attribute of the NPC and the NPC is with the player, the array is put back together as a single string and printed. You can use a vertical bar, |, to break the text into multiple paragraphs.

Used by several other functions, so this applies to them too. You can use run instead of text.

setItemAtt: Sets one attribute on the given item, it will guess if Boolean, integer or string. The first value is the item name, the second the attribute name, the third the value. The rest are passed to text.

moveItem: Moves the given item to the location. The first value is the item name, the second the location name. The rest are passed to text. If the location is "player", it will be the player is in whatever the name of the player object. Use "_" to have the item go nowhere (loc = false).

Waiting...

wait: Do nothing for one turn. Or give it a number, to wait that many turns.

waitFor: If the first value is a function attribute of the NPC, then the function is called - the other values are passed to that function. If the function returns true, this will be removed from the agenda, otherwise it will stay there to be called again next turn.

If the first value is not a function attribute of the NPC, wait until the object named in the first value (or the player, if the first value is "player") is here, then print the rest of the array using text. This may be repeated any number of times.

waitWhile and waitUntil: The first value should be the name of an object, or "player", the second value should be the name of an attribute on that object. The NPC will while the attribute has the given value or until it has that value.

waitForNow, waitWhileNow and waitUntilNow: These are the same as waitFor , waitWhile and waitUntil except that the next agenda item will run on the same turn that the condition is met, rather than the next turn.

Groups

joinedBy: The first value should be the name of an NPC. The named NPC will become a follower of this NPC. The other values are passed to text.

joining: The first value should be the name of an NPC. The this NPC will become a follower of the named NPC. The other values are passed to text.

disband: All followers of this NPC stop being followers. All the values are passed to text.

Moving

moveTo: Moves this NPC to the location, as long as there is an exit (you will get an error if not). A message will be printed if the player is at the origin or destination. The other values are passed to text.

jumpTo: Moves this NPC directly to the location (i.e., it will take one turn to get there no matter the distance or even when there are no connecting rooms). No messages will be printed, and no reactions will trigger - all it does is set the "loc" attribute. The other values are passed to text.

If the location is "player", it will be to the room the player is in whatever the name of the player object. Use "_" to have the NPC go nowhere (loc = undefined).

patrol: Moves this NPC on a patrol route. Each value should be a room on that route, and the NPC will jump from one to the next in a single turn using the moveTo action - so the same rules apply. The NPC will continue to patrol indefinitely. To temporarily stop the NPC patrolling you can send his "suspended" attribute to true. To permanently stop it, and allow the next item in the agenda to be used, remove this from the agenda array: npc.agenda.shift().

walkRandom: Moves this NPC to a random connected location (i.e., a location connected to its current location via an exit that is neither hidden nor locked). A message will be printed in the player is at the origin or destination. This will repeat indefinitely. If there are no available exits, then the other values are passed to text, and this action will terminate (the NPC will move on to the next action on the agenda). Can also be stopped like patrol.

walkTo: Move to the given location, using available, unlocked exits, one room per turn, then print the rest of the array as text. Use "player" to go to the room the player is in (if the player moves, the NPC will head to the new position, but will be omniscient!). Use an item (i.e., an object not flagged as a room) to have the NPC move to the room containing the item. This may be repeated any number of turns until the NPC reaches the destination. An error will be thrown if the destination is not reachable.

leadTo: As walkTo, but at each location the NPC will wait for the player to get there. This allows the NPC to leads the player to the destination.

Note on waitFor

You can add extra attributes to waitFor to have something trigger the turn the test comes true. The test function will be passed all the remaining parameters, which gives you the opportunity to make the function general. in the agenda below, a test function "checkCharArrivesAt" takes the name of an NPC and a location as parameters. There is a problem there that when the test passes, all those parameters get passed to the text instruction. It is going to try to find a function w.Lara.Kyle. the trick is to consume those parameters in the check function. In this we are using two, so we need two unshift calls on the array. Now "askForCarrots" will be passed to text.

createItem("Lara", NPC(true), {
  agenda:[
    'waitFor:checkCharArrivesAt:Kyle:kitchen:askForCarrots',
  ],
  checkCharArrivesAt:function(arr) {
    const c = w[arr[0]]
    const loc = arr[1]
    arr.shift()
    arr.shift()
    return c.loc === loc
  },
  askForCarrots:function(n) {
    this.msg("'Hi, Kyle, can I have some carrots?'") 
  },
})

The same applies to wait, but it is far simpler to just wait one less turn, and then have a text instruction. You may be able to do that with waitfor, but there is a good chance you need something to happen that very turn.

Custom agenda functions.

You can add your own functions to the agenda object like this (in the file code.js):

agenda.myCustomAgendaAction = function(npc, arr) {
  // do stuff
  return true
}

If it returns true, this action will get removed from the NPCs agenda, if it returns false it is not removed, and next turn this action will get repeated.

You can also return the string "next". This will force the next agenda item to happen immediately, without waiting for the player to have a turn. I suggest this is only used in functions that wait for something, and then allows the NPC to react immediately to it. If this function changes the world model in anyway, you would probably need to call world.update() before the return.

The npc is obviously the NPC doing the action, arr is an array of arguments, i.e., everything sent to the agenda item that was separated by colons. To illustrate, the above might be used like this:

"myCustomAgendaAction:Arthur:Some additional text."

The value of arr will thenb be an array ["Arthur", "Some additional text."].

Passing agendas

If you have several NPCs that follow the same agenda, you might want to just create it once and pass it to each. You need to bear in mind that the adenda array will get modified, so if one NPC completes the first item, that will get removed from the list they are all sharing, and all will move on to the next time.

If that is not what you want, you need to make a copy of the agenda.

w.Kyle.agenda = Array.from(agenda)

Debugging

Set settings.agendaDebugging to true to have agenda instructions noted on screen.

Reacting to what the player is doing

Sometimes you will want the agenda actions to take account of what the player is doing. One way to determine that is to check what the current command is. This is held in parser.currentCommand.

if (parser.currentCommand.name !== 'Juggle') msg("'May be a spot of juggling will distract them.'")

A note about processing time

Pathfinding requires a lot of calculation. According to here, this can take tens of seconds for the built-in function in TADS, so I was curious how this would compare. I set up a 10 by 10 maze, as described in that thread (though I added 10% of rooms have exits in both directions). I put the player in an isolated room, and 100 NPCs at the start of the maze. Then, using the unit testing library, had the WAIT function repeat 25 times, and compared the elapsed time when the NPCs were pathfinding every time as they navigated the maze (ca. 250 ms) and when they had no agenda (ca. 100 ms). This indicates that pathfinding on my PC takes a little under a millisecond, and even if you have a hundred of them, the effect is not going to be noticeable. In fact, it takes long to handle scoping than pathfinding.

In summary, pathfinding is fast with QuestJS!