First time Quest user - NPCs in Gameplay

StokeBunnies
19 Oct 2020, 21:03

Hi, hoping someone can help. I have built a simple 20 page maze, using N,S,W,E, and the player moves fine through the maze. What I'd like to do is have 3 NPCs which are on different pages whenever someone starts the game. I can see code for moving NPCs around, but can't see how to actually create one. The plan is that if the player meets one of the NPCs on the same page, the game will end. Thanks in advance. SB


mrangel
19 Oct 2020, 23:23

Is this a gamebook, or a text adventure?

If it's a text adventure, an NPC is just like any other object.

If it's a gamebook (implied by "pages", which a text adventure doesn't have), it's going to be harder because the editor only allows you to create pages, not objects. On the other hand, there's nothing to stop you from using a page as an object.

If you let us know which type of game you have used, I can go into more detail.


StokeBunnies
20 Oct 2020, 07:37

Hi mrangel,
Thks for the reply. Yes, it's a gamebook. I tried with a text adventure first, but couldn't see how to add graphics and audio, which is what I wanted. I've used Inform7 before, so know how a text adventure works, but liked the additional bells and whistles of the gamebook.
SB


mrangel
20 Oct 2020, 13:09

OK. Gamebook mode is a lot simpler and doesn't have most of Quest's features. But you could still do this. There are a few different ways you could do this; depending on how complex your situation is.

Using a game attribute

I think the easiest way to handle an NPC who could be in a random location would be making an attribute to track which room the NPC is in.

On the first page, you'd include a script something like:

locations = Split("page1;page2;page5;page7;page9")
game.john_location = GetObject (StringListItem (locations, GetRandomInt (1, ListCount (locations)) -1))

Then in the text of each page you could include something like:

{either player.parent = game.john_location:John is lounging on a chair in the corner.}

(this works because player.parent is the current page in gamebook mode. I think you have to use either rather than if because the text processor if can't compare two attributes)

Or if you want to run code (like ending the game) you'd put it in the script section of each page:

if (player.parent = game.john_location) {
  msg ("Ooops! John has caught you. You lose!")
  finish
}

This could be bad for some uses, because scripts run before the text of the page is displayed. So if you want to do something like show a message after describing the room, you'd want to put it in a function. You'd create a function named MeetJohn which does whatever needs to happen when you meet john, and then at the end of the page you'd put:
{either player.parent = game.john_location:{=MeetJohn}}

Editing the pages
If you have a lot of NPCs, or a lot of locations, it might be easier to do this programatically.

At the start of the game, you'd have a script like this:

locations = Split("page1;page2;page5;page7;page9")
foreach (npc, Split("John;Pete;Steve")) {
  randompage = GetObject (StringListItem (locations, GetRandomInt (1, ListCount (locations)) -1))
  randompage.description = randompage.description + "<br/><br/>{=Meet" + npc + "}"
}

That basically picks a random page and adds the code {=MeetJohn}, or {=MeetSteve} or whatever to the end of its text. You don't need to track where the NPC is, because they're part of the page now. You can put whatever you want into those functions, and it will run after the page displays.

Pretend it's a text adventure

If you want to use the NPC tutorials for text adventure mode, you just have to remember that pages and objects are the same thing. So create a page for each NPC, and you can move them around however you want. Some of the functions from text adventure mode might not exist, but you can work around most of them.

Probably the biggest problem with this would be that the player can't see if there's an NPC in the room. You'd probably want to add a script to make the NPC's description and script display if they're in the current room. For example, in your start script you could put:

foreach (page, AllObjects()) {
  if (HasString (page, "description")) {
    page.description = page.description + "{=HandleNPCs}"
  }
}

And then create a function HandleNPCs with type "string", doing something like:

page = player.parent
foreach (npc, GetDirectChildObjects (page)) {
  if (not npc = player) {
    if ((GetBoolean (npc, "runscript") or GetBoolean(npc, "runscriptonly")) and HasScript(npc, "script")) {
        do (npc, "script")
      }
    }
    // Stop if the player has moved
    if (not page = player.parent) {
      return ("")
    }
    if (page = npc.parent and not GetBoolean(npc, "runscriptonly")) {
      if (HasString(npc, "sound")) {
        if (LengthOf(npc.sound) > 0) {
          play sound (npc.sound, false, false)
          game.continuesound = GetBoolean(npc, "continuesound")
        }
      }
 
      if (HasString(npc, "picture")) {
        if (LengthOf(npc.picture) > 0) {
          picture (npc.picture)
          msg ("")
        }
      }
      if (HasString(npc, "youtube")) {
        if (LengthOf(npc.youtube) > 0) {
          JS.AddYouTube(npc.youtube)
          msg ("")
        }
      }
      msg (npc.description)
      msg ("")
    }
  }
}
return ("")

That's very rough code off the top of my head. But should mean that if you create a "page" for an NPC and then move them into any other page, the NPC's script and text (plus sound, youtube, etc) will be displayed after the main text for the page. Unfortunately, doing options in this way isn't quite so easy (because you can't have multiple options sections displayed at the same time)


StokeBunnies
20 Oct 2020, 17:48

Hi mrangel
Thanks for all of that.
I adapted this for my own theme

locations = Split("f05;f06;f08;f07;f09")
game.Ferdy_location = GetObject (StringListItem (locations, GetRandomInt (1, ListCount (locations)) -1))

I put that on the 'game' page, and the game runs OK

Once I put this on the pages as above,

picture ("07forest.jpg")
either player.parent = game.Ferdy_location:Ferdy appears from behind a tree.
picture ("eastpath.jpg")
msg ("East path")
picture ("southpath.jpg")
msg ("South path")
picture ("westpath.jpg")
msg ("West path")
TextFX_Typewriter ("You can go east, west or south from here", 100)
GetInput() {
if (result = "E" or result = "e") {
MovePlayer (f06)
}
if (result = "S" or result = "s") {
MovePlayer (f01)
}
if (result = "W" or result = "w") {
MovePlayer (f05)
}
}

I get this error

Error running script: Error compiling expression 'game.Ferdy_location:Ferdy appears from behind a tree.': SyntaxError: Unexpected character: :Line: 1, Column: 28

When i tried to use this code
if (player.parent = game.Ferdy_location) {
msg ("Ooops! Ferdy has caught you. You lose!")
finish
}

The entire program crashed with a Quest error which appeared on every page, and is too long for here, but starts

System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary.

Had to close everything and re-open

Kind regards SB


mrangel
20 Oct 2020, 18:43

The first time, you seem to have put a text processor directive in a script. Text processor directives like {either: are intended to be used in the text of a page, so you don't need to use a script. So if your page is already a script, you should use the script version.

That seems a very strange error to appear.
Does it give the error on every page, or just the ones with the code on?

KeyNotFoundException doesn't appear often. That's not a Quest error, that's a C# error, and normally indicates that you've found a bug in Quest itself. I can't think of anything that would cause that; and I can't replicate it here. Do you have the game available anywhere so I could take a look?


mrangel
20 Oct 2020, 19:00

Ah, it looks to be an odd bug.

Apparently in Gamebook mode you can't use finish to display a game over screen. You'll need to display your own message and end the game yourself. For most functions, referencing a function that doesn't exist would generate an error saying no such function. In the particular case of finish, it crashes the web editor immediately, and won't allow that page to be viewed or edited.

Anyway, this could easily be solved by making your own game over. In this case, I assume when the player is caught you no longer want them to be prompted where to go next. The simplest way to do this is probably by using an else clause. so your script would look like this:

picture ("07forest.jpg")
if (this = game.Ferdy_location) {
  msg ("Ooops! Ferdy has caught you. You lose!")
}
else {
  picture ("eastpath.jpg")
  msg ("East path")
  picture ("southpath.jpg")
  msg ("South path")
  picture ("westpath.jpg")
  msg ("West path")
  TextFX_Typewriter ("You can go east, west or south from here", 100)
  GetInput() {
    if (result = "E" or result = "e") {
      MovePlayer (f06)
    }
    if (result = "S" or result = "s") {
      MovePlayer (f01)
    }
    if (result = "W" or result = "w") {
      MovePlayer (f05)
    }
  }
}

StokeBunnies
26 Oct 2020, 09:26

Hi mrangel

Thanks a lot for all your help, it took me a while to sort out, but the game is up and running, and solvers have liked it.

Cheers SB