Library: NPCs

ThePix edited this page Nov 2, 2018 · 7 revisions

If you want to create interesting and believable NPCs for your game, there are broadly three things you need to consider:

  • Allow the player to converse with them
  • Allow the player to get them to do things
  • Have them act independently

There are various ways to handle conversations, one being ConvLib. For the other two, you can use NpcLib, which, as of version 2.0, will allow the player to give NPCs instructions.

The library can be downloaded from here:

Library

Once you have added the library to your game, you will see new tabs will appear for objects. Details on how to make NPCs act independently are on this page.

NpcType, MaleNpc and FemaleNpc

Go to the NPC tab of your NPC object. You will see various options for the type. "Group" is for independent NPCs, so discussed on the other page.

MaleNpc and FemaleNpc set up the NPC as a closed, transparent container. That may seem bizarre, but it means that the player gets told what the NPC is carrying, but cannot just take her belongings. It also sets the NPC to be a named male or female.

For example, suppose we want to create an NPC called Mary. Create a new object, and call it "Mary". Go to the NPC tab, and set it to be "Female NPC". Go to the Object tab, and delete "Open" and "Close" from the list of "Display verbs" (two of each!). Now if you go in game, you can tell Mary to pick something up, and when you look at her, you will see she is holding it.

You may not want NPCs to be containers or you might have an NPC that is not named. In that case use the basic "NpcType".

For example, suppose we want to create an NPC called Mysterious Guy. Create a new object, and call it "mysterious guy". On the Setup tab, set him to be "Male character". Go to the NPC tab, and set it to be "Generic Npc". Now if you go in game, you can tell the mysterious guy to pick something up. You will not see he is holding it, but you can ask him to give it to you to check.

The rest of this tutorial applies to both types of NPCs

Giving Commands

You now have an NPC that will do what the player asks. The NPC should respond to commands like:

mary, get ring

tell mary to give the ring to the mysterious guy

tell guy to follow me

tell guy to wait

guy, go north

mary, sit on couch

tell mary to stand up

tell mary to go to the garage

Okay!

By default, the NPCs will all say "Okay!" when you ask them to do something. We will look at getting them to refuse later, but first we will varying what they say when they agree. Go to the NPC tab, and modify the agreement message. Note that this will go inside quotes when displayed, but should end with either a comma or exclamation mark. You can use the text processor to mix it up a bit, for example:

{random:Sure!:No problem,:Okay,}

No way!

Sometimes you will want your NPC to say no. To do that, you need to edit the "Check agreement script" on the NPC tab (and to edit it, you will need to click the first icon above it to create a local copy).

The default is this:

this.complies = true

That will have the NPC do whatever is asked. To have her refuse every time, you can do this:

msg("'No way!' says Mary, turning up her nose.")
this.complies = false

Here we set complies to false so Quest knows not to do it, but we also need to provide a message to the player.

The script can access two local variables, "command" and "object" (the latter may not be defined for some commands), so we can have the NPC do some things but not others. This will need to be done as a long if/else if/else cascade, and the way to approach it is to add the more specific conditions first, getting more general down the list. You also need to decide what the default will be for the last step.

For example:

if (command = "take" and (object = "cake" or object = "gold ring")) {
  this.complies = true
}
else if ((command = "drop" or command = "give") and object = "gold ring") {
  msg("'My precious...' says Mary, holding the tight tightly to her chest.")
  this.complies = false
}
else if (command = "drop") {
  this.complies = true
}
else {
  msg("'No way!' says Mary, turning up her nose.")
  this.complies = false
}

So we have four sections. The first is for when Mary is asked to TAKE either the cake or the gold ring, and will make her agree to both. The second is for when Mary is asked to DROP or GIVE the gold ring - she will refuse. However the third says that any other time she is asked to drop, she will comply. Finally, for any other command she will refuse.

You can also make this dependent of the game state. Perhaps Mary will give the gold ring if the curse is lifted, and will do anything if she is friendly.

if (command = "take" and (object = "cake" or object = "gold ring")) {
  this.complies = true
}
else if ((command = "drop" or command = "give") and object = "gold ring" and not GetBoolean(Mary, "cursed lifted")) {
  msg("'My precious...' says Mary, holding the tight tightly to her chest.")
  this.complies = false
}
else if (command = "drop") {
  this.complies = true
}
else if (GetBoolean(Mary, "friendly")) {
  this.complies = truee
}
else {
  msg("'No way!' says Mary, turning up her nose.")
  this.complies = false
}

This can be used to finely tune an NPC's behavior, but does have the potential to get complicated! Do test thoroughly.

The full list of commands: take, drop, give, go, follow, wait, standup, siton, standon, reclineon

Furniture

The player can ask an NPC to sit, stand or recline on a piece of furniture (and as a bonus, the player can do so too). To get this to work, you will need to flag an object as furniture.

Let us suppose we have an object "settee". On the Items for NPCs tab, set its type to "Furniture". You can now tick boxes to indicate if the player and NPCs can sit on it, lie on it or stand on it. There are also default messages for the player that you can edit.

Note that there is no built in limit for furniture; it will allow seventeen people to be sat on the same chair.

TAKE and DROP

Previously we looked at whether the NPC would be willing to take or drop objects. The other thing to consider is whether the item can be picked up, etc. The default behavior for an item is to do as it would for the player. If the player can pick it up, so can the NPC.

There may be times the player can pick up something and NPCs cannot, or vice versa. More likely is when you have a script for when the player tries to pick up an item, perhaps because he can only take it if wearing special gloves. In these cases you need to explicitly state if an NPC can take the item.

Go to the Items for NPCs tab, and change "Take (NPC)" to Boolean, and tick or untick the box as appropriate. You can do the same for DROP.

For more control, you can set these to run a script. The script can use a local variable "npc". Note that the script will run after the system has checked the NPC is willing to do this. If the NPC refuses, the script is not used. Here is an example that will allow just one NPC to pick up the gold ring:

if (npc = Mary) {
  msg ("Mary picks up the ring with a strange gleam in her eye.")
  this.parent = npc
}
else {
  msg (CapFirst(GetDefiniteName(npc)) + " tries to pick up the ring, but it will not budge.")
}

Note that the script must move the ring (third line above), if the NPC is successful.

GIVE

As with TAKE and DROP, the system will first check if the NPC is willing to give. However, you can add a script to the NPC to give more control, or to have something usual happen.

Once the curse has been lifted, we want Mary to give the gold ring to the mysterious man. Mary is the one doing the giving, so go to her NPC tab. The GIVE scripts are at the bottom. Click the "+" to add a new one, and type in "gold ring" (this must be exactly as the item's name).

For the script we can use the "subject" local variable, which is the NPC the item will be given too. In the script below we check if that in the mysterious guy, and act accordingly:

if (subject = mysterious guy) {
  msg ("Mary hands the ring to the mysterious man, and suddenly he is bathed in light. You can see him clearly at last; he is the Prince of Alagan!")
  mysterious guy.alias = "Prince of Alagan"
  gold ring.parent = mysterious guy
}
else {
  msg ("'I'm not sure this ring is for " + subject.article + ".'")
}

Independent NPCs

The other half of the NPC library allows NPCs to act independently. That is all discussed on another page, but you may want to think about what happens when an NPC who has their own agenda is told to do something by the player. If he is a guard patrolling the castle, he can just say no, and carry on. Let us suppose Mary is brewing potions, and so has to collect ingredients, prepare them and so on, but perhaps she is more helpful than the guard, and will stop to do as you request.

We can use the Suspend function to have an NPC stop following their own agenda, and the best place to do that is their "Check agreement script". You can call Suspend repeatedly, so we just add that to every section of the script where she complies.

if (command = "take" and (object = "cake" or object = "gold ring")) {
  this.complies = true
  Suspend(this, null)
}
else if ((command = "drop" or command = "give") and object = "gold ring" and not GetBoolean(Mary, "cursed lifted")) {
  msg("'My precious...' says Mary, holding the tight tightly to her chest.")
  this.complies = false
}
else if (command = "drop") {
  this.complies = true
  Suspend(this, null)
}
else if (GetBoolean(Mary, "friendly")) {
  this.complies = true
  Suspend(this, null)
}
else {
  msg("'No way!' says Mary, turning up her nose.")
  this.complies = false
}

Now if Mary refuses, she will continue with her agenda, but if she agrees, she will stop what she is doing.

Maybe she she start again later... This is fraught with difficulties, so must be done with care. The problem is that her agenda will make assumptions about the world that may no longer be true. She may no longer be holding the belladonna, and she might now be sat on the four-poster in the bedroom, so it will seem strange if she suddenly starts finely chopping belladonna.

One solution is to limit what Mary will do, and not allow her to move to another room at the player's request or to give up the belladonna. Another solution is to run a script before re-starting her agenda to check where she should be. Alternatively, you could give her a brand new agenda, so it appears as though she continues what she was doing, but actually starts on something new (this would be bad if the player needs to have that potion later, as it may never get made).

There is also the question of when to restart the agenda. It would seem odd if the player told her to sit on the chair, she sits on it, and then gets straight up and goes back to what she was doing.

What we can do is set Suspend with a count. This will decrease each turn, and when it gets to zero, the agenda will no longer be suspended, and it will run either "resumeagenda" (if it exists) or "takeaturn". The former allows you to give the NPC a script that will test the game state, and perhaps add to the agenda. The latter is the normal agenda script, so will just cause the NPC to start up again.

Let us suppose that for Mary, we use this each time in her "Check agreement script":

  Suspend(this, 5)

If the player asks her to get the cake, she will do so, putting her agenda aside for five turns. If she agrees to do something else in that time, then the counter will reset and she will wait a further 5 turns. After that, she will continue her agenda from wherever she left off.

Let us suppose Mary could be sat on a chair in the bedroom. We can give her a "resumeagenda" script to get her back to the alchemy lab to make the potion (this assumes her agenda is in the one room only).

if (HasAttribute(this, "savedactions)) {
  oldlist = this.savedactions
  this.savedactions = null
}
else {
  oldlist = this.actions
}
this.actions = NewStringList()
if (not posture = null) {
  list add (this.actions, "Stand:player")
}
if (not this.parent = alchemy lab) {
  list add (this.actions, "GoTo:alchemy lab")
}
foreach (s, oldlist) {
  list add (this.actions, s)
}
do (o, "takeaturn")

So we are creating a new agenda. First we get the old action list. If the command used that, the original will be saved in "savedactions" (for example, telling an NPC to go to a room will set a new agenda). Otherwise, we just use "actions".

Then we check if she is in a posture; if she is, the first thing she has to do is stand up. Is she in the wrong room? If so, she needs to go to the lab. Then we need to copy all the items from her old agenda to this one. Finally we call her "takeaturn" to get her to do the first action on the list.

It is as easy as that!

Adding a GO TO command

It might be good to allow the player to tell an NPC to go to a specific room. This will use the agenda system, and if you have lots of NPCs with their own agendas, letting the player tell them to go somewhere, and thus forget their agenda could be bad.

However, let us suppose this is something we want. Create a new command and set it to use a "Regular expression". Paste in this:

^((tell|ask) (?<objectnpc>.*) to|(?<objectnpc>.*),) go to (?<object>.*)$

For more on regular expressions see here.

Obviously if we want the NPC to go there, this is not the current room, so we need to set the scope to "World" for the object, but not the npc.

objectnpc=all|object=world

Then the code. The basic format is to check the NPC is actually an NPC, then do any other checks. If that is okay, we check if the NPC is willing to do it. If so, go for it:

if (not DoesInherit(objectnpc, "NpcType")) {
  msg (DynamicTemplate("PosturesNotNpc", objectnpc))
}
else if (OTHER TEST) {
  msg (OTHER TEST MESSAGES)
}
else {
  do (objectnpc, "checkagreement", QuickParams("command", "YOUR COMMAND", "object", object))
  if (objectnpc.complies) {
    msg ("'" + objectnpc.yesmsg + "' says " + CapFirst(GetDefiniteName(objectnpc)) + ".")
    MAKE THE NPC DO THE ACTION
  }
}

In this case, the code is this:


if (not DoesInherit(objectnpc, "NpcType")) {
  msg (DynamicTemplate("PosturesNotNpc", objectnpc))
}
else if (GetRoute(objectnpc, object) = null) {
  msg ("'That's not somewhere I can get to.'")
}
else {
  do (objectnpc, "checkagreement", QuickParams("command", "goto", "object", object))
  if (objectnpc.complies) {
    msg ("'" + objectnpc.yesmsg + "' says " + CapFirst(GetDefiniteName(objectnpc)) + ".")
    objectnpc.actions = Split("GoTo:" + object.name)
  }
}

If you do have NPCs with agendas, one option is to have them refuse to do this in the "Check agreement script".

Note that this uses the same system as the GoTo for agendas, so the same rules apply with regards to blocking exits to NPCs. If there is no route to a room (or the player is telling her to go to an item, not a room, the NPC will just say no.

Testing

NPCs are complicated, even with this framework, and good testing is vital.

To make that easier you may find it useful to create a testing room, and move the NPCs and the player object there temporarily, and make sure they behave as they should.

It is just as important to test that they refuse to act when they should.

You may find it useful to set up a test regime in the game start up script. This could move the player and the NPCs to the testing room, then run the commands to test. Here is an example:

player.parent = testing room
Mary.parent = testing room
mysterious guy.parent = testing room
HandleSingleCommand ("mysterious guy, get ring")
HandleSingleCommand ("mary, get ring")
HandleSingleCommand ("mary, give ring to me")
HandleSingleCommand ("mary, give ring to guy")

Now when the game is started, those four commands will be run, and you can see what the responses are and the game state afterwards.

Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.