Exercise 20 - Actors and Conversation (adv3)

LIGHTHOUSE is an adv3 demo designed to illustrate implementing NPCs in adv3Lite. It's loosely inspired by the Bop character used as an example in Mike Roberts's "Creating Dynamic Actors" article in the Technical Manual.

There's a couple of endings you can reach, but you can't really win. As with all these demos the point isn't playing the game but learning from the sourc code (although as ever it's worth trying to play it to see what the code does).

The game follows the specification of Exercise 20 in Learning TADS 3:

Both the NPC articles in the TADS 3 Technical Manual and several of those in this chapter refer to a character called Bob who stacks cans and mutters darkly about a lighthouse and some “troubles”, so try writing a small game based on that. Here’s some further suggestions: The player character is new to the town and has just gone into Bob’s shop, where Bob is busily stacking cans and a blonde woman (let’s call her Sally) is inspecting the clothes on the clothes rack. Sally is too interested in the clothes to engage a stranger in conversation, but Bob is more talkative. You can ask him about a number of topics, but if you ask him about the town, he’ll mention the lighthouse and the troubles, and then clam up on those topics. When Sally hears Bob mention the troubles she goes outside. When the player character leaves the shop Sally comes up to him and asks whether he wants her to show him the lighthouse. The player can reply yes or no or ask why she’s offering. If the player replies yes, Sally leads the player character to the lighthouse and then invites him to lead the way inside. On the lowest floor of the lighthouse there’s nothing but an abandoned storeroom and an abandoned office, which Sally comments on the first time she follows the player character there. Halfway up a spiral staircase is an oak door. When the player character goes through the door he meets more than he bargained for.

#charset "us-ascii"

/*   
 *   LIGHTHOUSE is a TADS 3 demo designed to illustrate implementing NPCs in 
 *   TADS 3. It's loosely inspired by the Bop character used as an example in
 *   Mike Roberts's "Creating Dynamic Actors" article in the Technical 
 *   Manual.
 *
 *   There's a couple of endings you can reach, but you can't really win. As 
 *   with all these demos the point isn't playing the game but learning from 
 *   the sourc code (although as ever it's worth trying to play it to see 
 *   what the code does).
 */


/*
 *   Copyright (c) 1999, 2002 by Michael J. Roberts.  Permission is
 *   granted to anyone to copy and use this file for any purpose.  
 *   
 *   This is a starter TADS 3 source file.  This is a complete TADS game
 *   that you can compile and run.
 *   
 *   To compile this game in TADS Workbench, open the "Build" menu and
 *   select "Compile for Debugging."  To run the game, after compiling it,
 *   open the "Debug" menu and select "Go."
 *   
 *   This is the "advanced" starter game - it has only the minimum set of
 *   definitions needed for a working game.  If you would like some more
 *   examples, create a new game, and choose the "introductory" version
 *   when asked for the type of starter game to create.  
 */

/* 
 *   Include the main header for the standard TADS 3 adventure library.
 *   Note that this does NOT include the entire source code for the
 *   library; this merely includes some definitions for our use here.  The
 *   main library must be "linked" into the finished program by including
 *   the file "adv3.tl" in the list of modules specified when compiling.
 *   In TADS Workbench, simply include adv3.tl in the "Source Files"
 *   section of the project.
 *   
 *   Also include the US English definitions, since this game is written
 *   in English.  
 */
#include <adv3.h>
#include <en_us.h>

/*
 *   Our game credits and version information.  This object isn't required
 *   by the system, but our GameInfo initialization above needs this for
 *   some of its information.
 *   
 *   You'll have to customize some of the text below, as marked: the name
 *   of your game, your byline, and so on.
 */
versionInfo: GameID
    IFID = 'f5a02ab4-71ec-80f5-83be-a83324c66bac'
    name = 'The Lighthouse'
    byline = 'by Eric Eve'
    htmlByline = 'by <a href="mailto:eric.eve@hmc.ox.ac.uk">
                  Eric Eve</a>'
    version = '0.3'
    authorEmail = 'Eric Eve <eric.eve@hmc.ox.ac.uk>'
    desc = 'A short game to demonstrate programming NPCs in TADS 3'
    htmlDesc = 'A short game to demonstrate programming NPCs in TADS 3'
;

/*
 *   The "gameMain" object lets us set the initial player character and
 *   control the game's startup procedure.  Every game must define this
 *   object.  For convenience, we inherit from the library's GameMainDef
 *   class, which defines suitable defaults for most of this object's
 *   required methods and properties.  
 */
gameMain: GameMainDef
    /* the initial player character is 'me' */
    initialPlayerChar = me
    showIntro()
    {
        "You've only just moved into town, so you thought you'd pay the local
        store a visit to pick up some basic essentials. But you may be about to
        get more than you bargained for.\b";
    }
    
    showGoodbye()
    {
        if(me.hasSeen(horrorChamber))
            "<.p>Sorry, that's all folks! ";
        else
            "<.p>Perhaps you should try a bit harder next time! ";
    }
;

/* 
 *   Starting location - we'll use this as the player character's initial
 *   location.  The name of the starting location isn't important to the
 *   library, but note that it has to match up with the initial location
 *   for the player character, defined in the "me" object below.
 *   
 *   Our definition defines two strings.  The first string, which must be
 *   in single quotes, is the "name" of the room; the name is displayed on
 *   the status line and each time the player enters the room.  The second
 *   string, which must be in double quotes, is the "description" of the
 *   room, which is a full description of the room.  This is displayed when
 *   the player types "look around," when the player first enters the room,
 *   and any time the player enters the room when playing in VERBOSE mode.
 *
 */
shop: Room 'General Store'
    "The shop has much the sort of stuff you'd expect in a general store in a
    sleepy sea-side town like this. The only way out is to the north. "
    north = street
    out asExit(north)
    
    /* 
     *   followDesc is a custom property we are defining for use with an 
     *   accompanying NPC (see below). It's used to customize what's 
     *   displayed when the sally NPC accompanies the PC via a particular 
     *   connector.
     */
    followDesc = 'back into the shop'
;

/*   
 *   Since this is a demo of NPCs, we'll keep the implementation of the map 
 *   and the physical objects in it as basic as possible, more or less to 
 *   the bare minimum needed to illustrate the various features of NPC 
 *   programming. For the same reason we'll only provide minimal commenting 
 *   for the map-building section of the code.
 *
 *   We don't need multiple rooms to illustrate conversation, but we do need 
 *   multiple rooms to illustrate an NPC who accompanies the PC on his 
 *   travels, so we'll still need several rooms in this map.
 */


+ Decoration 'clothes rack' 'clothes rack'
    "It's a rack with lots of clothes on. "
;

+ cans: Decoration 'carefully stacked stack/can/cans' 'cans'
    "The cans are all carefully stacked. "
    isPlural = true
;


/*
 *   PLAYER CHARACTER ACTOR
 *
 *   Define the player character.  The name of this object is not important, 
 *   but it MUST match the name we use to initialize 
 *   gameMain.initialPlayerChar above.
 *
 *   Note that we aren't required to define any vocabulary or description 
 *   for this object, because the class Actor, defined in the library, 
 *   automatically provides the appropriate definitions for an Actor when 
 *   the Actor is serving as the player character.  Note also that we don't 
 *   have to do anything special in this object definition to make the Actor 
 *   the player character; any Actor can serve as the player character, and 
 *   we'll establish this one as the PC in main(), below.  
 */
+ me: Actor
;

street: OutdoorRoom 'Main Street'
    "This looks the main street running through the town. At this point it runs
    past the general store immediately to the south. The town centre lies to the
    west, while to the east the street runs on out of town. "
    south = shop
    in asExit(south)
    west: DeadEndConnector { 
        -> 'the town centre'
       "You wander round the town centre for a while, but find nothing that
       holds your attention, so you eventually find your feet bringing you
       back where you started. " }
    east: OneWayRoomConnector {
        -> road
        followDesc = 'along the street all the way out of town, and then on
            down the road'
    }
;

road: OutdoorRoom 'Road'
    "The road seems unusually quiet, with very little traffic; right now
    there's none in sight at all. It runs fairly straight here, almost dead
    straight back into the town to the west, although you can see it start to
    bend some way off to the east. An open field lies to the south. "
    west = street
    east: DeadEndConnector { "You walk a short way down the road, then turn
        round and come back. "}
    south: OneWayRoomConnector {
        ->hillTop
        followDesc = 'away from the road, across a field and up a shallow hill'
    }
;

+ Enterable ->(road.south) 'open field' 'open field'
    "It slopes gently up to the east. "
;


hillTop: OutdoorRoom 'Hilltop'
    "From the top of the hill you can clearly see the sea glinting a short way
    to the south. It looks possible to descend the hill to the southeast, the
    southwest, or the north. "
    southeast: OneWayRoomConnector {
        -> cliff
        followDesc = 'down the far side of the hill, and carry on a short way
        towards the coast'
    }
    southwest: DeadEndConnector {
        -> 'the sheer cliff'
        "You walk a short way down to the southwest, but soon come to a sheer
        cliff overlooking the sea. Since you can go no further in that direction
        you turn round and come back. "
    }
    north = road
    followDesc = 'back up the hill'
;

MultiLoc, Distant 'wet watery calm sea' 'sea'
    "It looks as wet and watery as the sea always does, though it also looks
    reasonably calm. "
    locationList = [cliff, cliffPath, hillTop]
    aName = 'the sea'
;

cliff: OutdoorRoom 'Lonely Cliff'
    "The path down the hillside to the northwest comes to an end on this bleak
    stretch of cliff, overlooking the sea several dozen yards below. An old
    lighthouse, perched strategically near the cliff-edge, stands immediately
    to the east. To the west a narrow path snakes precariously along the
    cliff-top. "
    northwest = hillTop
    
    east = lhDoor
    in asExit(east)
    west = narrowPath
;

+ lighthouse: Enterable ->lhDoor 'damaged old lighthouse' 'lighthouse'
    "The lighthouse is still standing, but it looks as if the upper part has
    sustained some damage. "
;

++ lhDoor: Door 'door' 'door'
    followDesc = 'through the door into the lighthouse'
;

+ narrowPath: PathPassage 'narrow path' 'narrow path'
    "It snakes precariously along the cliff-top to the west. "
    destination = cliffPath
    followDesc = 'down the narrow path'
;

cliffPath: OutdoorRoom 'Cliff Path'
    "The path from the northwest comes to a dead end overlooking the sea. "
    northwest = cliff    
;

lobby: Room 'Lighthouse Lobby'
    "This would seem to be the entrance lobby for the lighthouse. A spiral
    staircase winds up to the north, and the way out is to the west, while a
    second exit leads east. "
    west = lhDoorInside
    out asExit(west)
    north = lhStair
    up asExit(north)
    east = store
    
    followDesc = 'back into the lobby'
;

+ lhDoorInside: Door ->lhDoor 'door' 'door'
    followDesc = 'back out through the door'
;

+ lhStair: StairwayUp 'spiral flight/stairs/staircase' 'flight of stairs'
    followDesc = 'up the stairs'

;    

store: Room 'Storeroom' 
    "This looks as if it was once a storeroom for the lighthouse, but it's been
    stripped almost bare now. The exit back to the lobby is to the west, but
    there's another exit to the south. "
    west = lobby
    south = office
    out asExit(west)
    followDesc = 'into the storeroom'
;

office: Room 'Office'
    "The battered old wooden desk standing in the corner suggests that this was
    once an office of some sort, but there's little else here now. The only way
    out is to the north. "
    north = store
    out asExit(north)
    followDesc = 'into the office'
;

+ Surface, Heavy 'battered old wooden desk' 'desk'
    "It has certainly seen better days. "
;


midStair: Room 'On the Staircase'
    "About halfway up the lighthouse the spiral staircase passes an oak door
    to the east. "
    down = midStairDown
    up = midStairUp
    east = oakDoor
;

+ midStairDown: StairwayDown ->lhStair 'descending down spiral staircase/stairs' 
    'descending staircase'
    followDesc = 'back down the staircase'
;

+ midStairUp: StairwayUp 'ascending up spiral staircase/stairs' 
    'ascending staircase'
    dobjFor(TravelVia)
    {
        action()
        {
            "You only get a short way further up the staircase before
            discovering that the way has been blocked by falling masonry, so
            you are forced to turn round and come back. ";
        }
    }
;

+ oakDoor: Door 'oak door*doors' 'oak door'
    followDesc = 'through the door'
;

horrorChamber: Room 'Chamber of Horrors'
    /* 
     *   Normally it would be a bad idea to include a description of the PC's
     *   first reaction to a location in the desc property (we should 
     *   normally use roomFirstDesc for this), but since this room 
     *   description will only ever be displayed once, when the PC first 
     *   enters this room, it doesn't matter here.
     */
    "The sight that meets your eyes as you walk into this chamber astonishes,
    disgusts and terrifies you all at once. You are literally struck dumb with
    horror. You didn't know what to expect, but never in your wildest nightmares
    did you ever imagine anything like <i>this</i>..."
    east = horrorDoor
    out asExit(east)
;

+ horrorDoor: Door ->oakDoor 'door' 'door'
;

//==============================================================================
/*   
 *   CLASS DEFINITION
 *
 *   Later in the game we'll be creating Can objects dynamically, so we need 
 *   to define a Can class.
 */

class Can: Thing 'round aluminium tin can/tin*cans*tins' 'can'
    "It's round and made of tin -- or maybe aluminium. "
    
    /* One can is much like any other - we don't need to distinguish them. */
    isEquivalent = true
    
    /* 
     *   We want to keep track of the total number of Cans we've created, so 
     *   we do this in the construct() method. Note that we keep this count 
     *   on the Can class, not on individual Can objects.
     */
    construct()
    {
        inherited();
        Can.cansCreated ++;
    }
    cansCreated = 0
;


//==============================================================================
/*  
 *   THE MAIN NPC CODE STARTS HERE
 *
 *   There'll be two NPCs in this game. Bob, the shopkeeper, will be used to 
 *   demonstrate the standard method of implementing conversations in TADS 
 *   3. Sally, the other, will be used to illustrate an NPC who accompanies 
 *   the PC on his travel, as well as other features such as AgendaItems and 
 *   Conversation Nodes. 
 *
 *
 *   We'll start by modifying the standard library Person class to provide 
 *   some tweaks we should find useful.
 */


modify Person
    /* 
     *   We often don't know the name of NPCs when we first encounter them, 
     *   so that to begin with they have generic descriptions like 'a tall 
     *   man' or 'a blonde woman'. But once the PC knows an NPC's proper 
     *   name, that's the name that should be used. makeProper() is a custom 
     *   method we define to handle giving an NPC a proper name once we know 
     *   what it is.
     */
    
    makeProper()
    {
        /* 
         *   isProperName is a standard library class; by setting it to true 
         *   we tell the parser not to display an article ("a" or "the") 
         *   with the actor's name; "a man" or "the woman" is fine, but once 
         *   they have proper names they should be called "Bob" and "Sally", 
         *   not "a Bob" or "the Sally".
         */
        isProperName = true;
        
        /*   
         *   Set the name property (a standard library property) to the 
         *   value of the properName property (a custom property we define 
         *   below, and which would need to be overridden on each actor with 
         *   the name of that actor).
         */
        name = properName;
        
        /*   
         *   Ensure that the parser will recognize the properName as 
         *   referring to this actor.
         */
        initializeVocabWith(properName.toLower());
        
        /*   
         *   Return the properName to the caller, so it can be used in 
         *   constructions like "<q>Hi, I'm <<properName>>,</q> the man 
         *   introduces himself.
         */
        return properName;
    }
    
    /* 
     *   properName is a custom property we define to contain the NPC's 
     *   proper name -- what he or she will be known by once the PC knows 
     *   his or her name.
     */
    properName = nil

    /*  
     *   The first time an actor's mentioned we expect him or her to be 
     *   described with the indefinite object ("A tall man is standing 
     *   here"); thereafter we'd expect to see the definite article ("The 
     *   tall man is standing here"). We therefore override theName (and add 
     *   a custom previouslyMentioned property) to produce this effect; the 
     *   first time theName is called it will return aName; thereafter it 
     *   will return the inherited version of theName.
     */
    
    timesMentioned = 0
    theName = (timesMentioned++ ? inherited : aName)
    
    /*  
     *   There aren\'t many portable objects in this game (unless the player 
     *   start collecting cans from Bob), but the standard library response 
     *   for throwing things at things is quite inappropriate when the 
     *   target is a person, (e.g. "The can hits Bob without any obvious 
     *   effect and falls to the floor") that we should in any case override 
     *   it.
     */
    
    iobjFor(ThrowAt)
    {
        check()
        {
            failCheck('You\'re not a child throwing a tantrum; you know better
                than to start throwing things at people. ');
        }
    }
    
;

//==============================================================================
/*  
 *   CODE FOR THE BOB NPC 
 *
 *   There's usually a lot of code and objects associated with an NPC, so 
 *   where an NPC is at all complex it's best to keep the NPC code separate 
 *   from the rest of the room and game code.
 *
 *   PERSON
 *
 *   There's not much to define on the Person object itself, since most of 
 *   Bob's behaviour will be implemented in his ActorStates and Topic 
 *   Entries. 
 */

bob: Person 'tall man*people' 'tall man' @shop
    "He's a man with a gaunt face and a slight stoop; you'd estimate he was
    nearer sixty than fifty. "
       
    /* Remember, this is a custom property. We just defined it above/ */
    properName = 'Bob'
    
    /* Bob is male. */
    isHim = true
    
    /* 
     *   It's useful to define a globalParamName on an NPC, so we can refer 
     *   to him or her in paramater substitution string (such as '{the 
     *   bob/he}'). This is particular useful when the name of the NPC might 
     *   change in the course of play (e.g. from 'the tall man' to 'Bob' if 
     *   and when we learn Bob's name), but we don't know in advance when 
     *   this will happen. 
     */
    globalParamName = 'bob'
    
    /*   Customize the responses for ATTACK/HIT/KICK/KILL BOB and KISS BOB */    
    uselessToAttackMsg = 'There seems to be no call for violence. '
    cannotKissActorMsg = 'Neither of you would enjoy that very much. '
    
;

/*  
 *   UNTHING
 *
 *   It's debatable whether the player who already knows that the tall man is
 *   called Bob should be able to refer to him as 'Bob' before the player 
 *   character has officially learned his name. In this example we're 
 *   assuming not, but you may well prefer to do things differently in your 
 *   own games, (in which case just include 'bob' or whatever in the NPC's 
 *   vocabWords property when you first define him or her.
 *
 *   But the problem then is what to do when the player does try to refer to 
 *   Bob before the PC has learned his name. If we haven't defined 'bob' in 
 *   bob's vocabWords the game will respond with "The word “bob” is not 
 *   necessary in this story. ", but that's simply untrue!
 *
 *   One way to get round this is with this little trick using an Unthing, 
 *   which we attach to Bob so it's always in scope when Bob is in scope. 
 *   Then if the player tries to refer to Bob before Bob has been named, 
 *   s/he'll get the response "You don't know anyone called Bob yet", which 
 *   is at least better than the default. Once Bob has named, the Unthing 
 *   will simply be ignored (since the parser always prefers any other 
 *   object to an Unthing if one is available).
 */


+ Unthing 'bob' 'bob'
    'You don\'t know anyone called Bob yet. '
;

/*   
 *   IN CONVERSATION STATE
 *
 *   To implement a conversation with Bob, we start by defining his 
 *   InConversationState, the ActorState he's in when conversing with the 
 *   player character
 */


+ bobTalking: InConversationState
    /* 
     *   The specialDesc defines how Bob will be described in a room 
     *   description when he's in this ActorState. Note the use of {The 
     *   bob/he} which will expand to either 'Bob' or 'The tall man', 
     *   depending on whether or not the PC has yet learned Bob's name.
     */
    specialDesc = "{The bob/he} is standing facing you, with one hand on his
        hip and the other holding a can. "
    
;

/*  
 *   CONVERSATION READY STATE
 *
 *   The ConversationReadyState is the ActorState Bob is in when he's not 
 *   actually engaged in conversation, but can be. When the PC talks to Bob, 
 *   Bob will switch to the associated InConversationState.
 *
 *   We could set the appropriate properties of bobTalking and bobStacking to
 *   point to each other, but locating the ConversationReadyState in the 
 *   InConversationState saves us the bother. This way, the library knows 
 *   that bobStacking is the ConversationReadyState associated with 
 *   bobTalking (and vice versa).
 *
 *   To keep things simple we are defining only one ConversationReadyState/ 
 *   InConversationState pair for Bob, but in your own game you can have as 
 *   many as you like. To switch ActorStates under programmatic control use 
 *   bob.setCurState(newState).
 */

++ bobStacking: ConversationReadyState
    /* This is the state Bob starts out in. */
    isInitState = true
    
    /* 
     *   commonDesc is not a library property; it's a custom property we're 
     *   defining to avoid the need to duplicate text in the specialDesc and 
     *   stateDesc properties.
     */
    commonDesc = 'busily stacking cans at the rear of the shop'
    
    /*   
     *   How Bob will be mentioned in a room description when he's in this 
     *   state.
     */
    specialDesc = "{The bob/he} is <<commonDesc>>. "
    
    /*   
     *   The stateDesc will be added to the end of the description provided 
     *   by bob.desc when Bob is in this ActiveState and the player examines 
     *   Bob.
     */
    stateDesc = "He's <<commonDesc>>. "    
;

/*
 *   HELLO TOPIC
 *
 *   A HelloTopic is used to handle Bob's response to BOB, HI, or TALK TO 
 *   BOB, when Bob is in this ConversationReadyState. Since we're not also 
 *   defining an ImpHelloTopic it will also be used when the PC addresses 
 *   any conversational command to Bob (e.g. ASK MAN ABOUT THE WEATHER) 
 *   while he's in the ConversationReadyState, in which case the 
 *   conversational exchange defined in the HelloTopic will occur before the 
 *   specific topic asked about is handled (and Bob will then switch into his
 *   InConversationState. 
 *
 *   Note that HelloTopics (and ByeTopics) are normally located in a 
 *   ConversationReadyState.
 *
 *   By combining HelloTopic with  StopEventList we can vary the response Bob
 *   gives each time he is addressed. 
 */

+++ HelloTopic, StopEventList
    [
        '<q>Hello there!</q> you say.\b
        <q>Good morning, sir,</q> he replies, turning to you. ',
        
        '<q>Excuse me,</q> you say.\b
        <q>Yes?</q> he replies, turning to face you. ',
        
        '<q>Hello again,</q> you greet him.\b
        <q>Good morning once more, sir,</q> he answers, as he turns to face
        you. '
    ]
;

/*
 *   BYE TOPIC
 *
 *   We use a ByeTopic to end a conversation just as we use a HelloTopic to 
 *   begin with. Since we're also defining an ImpByeTopic, this ByeTopic will
 *   only be triggered in the player explicitly bids Bob goodbye (e.g. with 
 *   a BYE command). Note that like HelloTopics, ByeTopics are normally 
 *   located in a ConversationReadyState. If the PC bids Bob goodbye while 
 *   Bob is in the corresponding InConversationState, Bob will switch to the 
 *   ConversationReadyState and this farewell message will be displayed.
 */    
+++ ByeTopic
    "<q>Well, cheerio then!</q> you say.\b
    <q>Good day, sir,</q> {the bob/he} replies, before returning to his stack
    of cans. "
;

/*   
 *   IMP BYE TOPIC
 *
 *   An ImpByeTopic is just like a ByeTopic, except that it is triggered 
 *   when a conversation is terminated other than with an explicit GOODBYE. 
 *   This may happen if the Player Character simply walks away in the middle 
 *   of the conversation, or if s/he fails to address Bob for so many turns 
 *   (defined in the InConversationState's attentionSpan property) and Bob 
 *   gets bored waiting for the PC to speak. If we want to distinguish 
 *   between these two implicit Bye cases we can use LeaveByeTopic and 
 *   BoredByeTopic, but here we'll just use an ImpByeTopic to cover both 
 *   cases. 
 */

+++ ImpByeTopic
    "{The bob/he} turns away and resumes stacking cans. "
;

/*   
 *   ASK TOPIC
 *
 *   SUGGESTED ASK TOPIC
 *
 *   Note that we've finished with the ConversationReadyState: all the 
 *   following TopicEntries are located in the InConversationState.
 *
 *   An AskTopic is used to respond to a question such as ASK BOB ABOUT 
 *   WHATEVER (which can be abbreviated to A WHATEVER one a conversation is 
 *   underway). The AskTopic defined below will match the question ASK BOB 
 *   ABOUT LIGHTHOUSE (because 'lighthouse' is defined in the vocabWords of 
 *   the lighthouse object). But note that the PC must first know about the 
 *   lighthouse before this topic becomes available.
 *
 *   By making this AskTopic also a SuggestedAskTopic we include it in the 
 *   topic inventory - the list of suggested topics for conversation 
 *   displayed in response to a TOPICS command or to TALK ABOUT BOB. Note 
 *   that the topic will only be suggested when the TopicEntry becomes 
 *   available, i.e. when the PC knows about the lighthouse.
 */
++ AskTopic, SuggestedAskTopic @lighthouse
    "<q>So what happened at the lighthouse?</q> you wanted to know.\b
    <q>Well,</q> he begins, <q>it was like this -- all those years ago...</q> he
    breaks off, as if wrestling with himself, then suddenly snaps, <q>Just don't
    go there, that's all!</q> "
    
    /* The name we use to describe this topic in a list of suggested topics. */
    name = 'the lighthouse'
;

/*
 *   TELL TOPIC
 *
 *   SUGGESTED TELL TOPIC
 *
 *   TellTopic is the converse of AskTopic. It defines the response to a 
 *   command like TELL BOB ABOUT WHATEVER (in this case TELL BOB ABOUT 
 *   MYSELF or T MYSELF). We combine it with SuggestedTellTopic so that it 
 *   appears in a list of suggested topics (note we don't have to do this; 
 *   it's up to the game author whether a particular TopicEntry should be 
 *   suggested or not). 
 *
 *   By combining the TellTopic with ShuffledEventList we can vary the 
 *   response. This is often a good idea for creating more life-like NPCs.
 */
++ TellTopic, SuggestedTellTopic, ShuffledEventList @me
    /* 
     *   When we define two lists for a ShuffledEventList, the items in the 
     *   first list are first shown in order.
     */
    [
        '<q>I\'m new around here,</q> you say.\b 
        <q>That probably explains why we haven\'t met before,</q> he remarks. '
    ]
    
    /*  
     *   After the items in the first list have been shown, the items in the 
     *   second list are displayed in shuffled-random order. We don't need to
     *   define both lists; if we only define one, it will be used as a 
     *   shuffled list.
     */    
    [
        '{The bob/he} listens politely while you tell him a chunk of you
        life-history. ',
        
        '{The bob/he} does his best to pretend he finds you as fascinating a
        topic of conversation as you appear to. ',
        
        '<q>How interesting!</q> he mutters. '
    ]
    
    /* The name for the list of suggested topics */
    name = 'yourself'
;

/*  
 *   Another ASK TOPIC
 *
 *   This time we mix it in with a StopEventList, which will display each 
 *   item in turn and then keep displaying the last one.
 *
 *   The @bob means the bob object, of course, but while we're talking to bob
 *   this can be referred to as 'himself', so this topic will match ASK BOB 
 *   ABOUT HIMSELF.
 */
++ AskTopic, SuggestedAskTopic, StopEventList @bob
    [
        /* 
         *   We can't include a double-quoted string in an EventList, but we 
         *   can include a function pointer, which is what wrapping a 
         *   double-quoted string in the {: } short-form anonymous function 
         *   notation actually does. This enables us to invoke 
         *   bob.makeProper() using the <<>> syntax; bob.makeProper() 
         *   returns 'Bob', so this is what it displays, but it also carries 
         *   out the corresponding changes on the bob object.
         *
         *   An alternative would have been to write:
         *
         *   '<q>By the way, I don\'t think I caught your name,</q> you 
         *   say.\b <q>I\'m '+ bob.makeProper() + ',</q> he tells you. '
         */        
        {: "<q>By the way, I don't think I caught your name,</q> you say.\b
            <q>I'm <<bob.makeProper()>>,</q> he tells you. " },
        
        '<q>Have you worked here long?</q> you ask.\b
        <q>Only the last forty years,</q> he replies. ',
        
        'You can\'t think of anything to ask him about himself right now. '
    ]    
    name = 'himself'
    
    /* 
     *   Once the player has seen the first two responses, the response to 
     *   ASK BOB ABOUT HIMSELF ceases to be interesting, so there's no point 
     *   in suggesting it any longer. So we'll only suggest it twice (note 
     *   that the default value of timesToSuggest is 1).
     */
    timesToSuggest = 2
    
    /*
     *   Normally ASK BOB ABOUT WHATEVER would trigger the greeting protocols
     *   (the HelloTopic) when Bob is in his ConversationReadyState. But the
     *   final response suggests that no conversational exchange actually 
     *   takes place, so it would be inappropriate to display a greeting 
     *   only to have a message say that you can't think of anything to ask. 
     *   We can avoid this by marking this TopicEntry as non-conversational 
     *   once the final response is reached. Since we've already defined 
     *   timesToSuggest = 2 this is equivalent to marking it as 
     *   non-conversational once curiosity has been satisfied.
     */    
    isConversational = (!curiositySatisfied)
    
;

/*  
 *   ASK TELL SHOW TOPIC
 *
 *   An AskTellShowTopic responds to ASK ABOUT, TELL ABOUT and SHOW 
 *   commands, so that this one will respond to ASK BOB ABOUT BLONDE, TELL 
 *   BOB ABOUT BLONDE, or SHOW BLONDE TO BOB.
 */
++ AskTellShowTopic, SuggestedAskTopic, StopEventList @sally
    [
        {: "<q>Who's that blonde woman over there?</q> you ask.\b
            <q>That's <<sally.makeProper()>>, sir,</q> he replies. "
        },
        
        '<q>Is {the sally/she} a regular customer here?</q> you enquire.\b
        <q>Oh yes, sir, one of my best,</q> he tells you. ',
        
        'You\'ve asked enough about {the sally/her} for now; you don\'t want to
        give {the bob/him} the impression that you\'re fixated on her! '
    ]
    
    /* 
     *   We want this topic to be suggested as 'ask Bob about the blonde 
     *   woman' before we learn Sally's name and 'ask Bob about Sally' 
     *   thereafter.
     */
    name = (sally.theName)
    
    /*   
     *   The first question presupposes that Sally is present to be asked 
     *   about, so we make this TopicEntry active only when Sally is in the 
     *   shop.
     */    
    isActive = (sally.isIn(shop))
    timesToSuggest = 2
    isConversational = (!curiositySatisfied)
;

++ AskTopic, SuggestedAskTopic, StopEventList @tTown
    [
        /* 
         *   Until the player sees this response, the player character 
         *   hasn't even heard of the troubles. The tag <.reveal troubles> 
         *   notes the fact that the topic of troubles has now been 
         *   mentioned. Note that 'troubles' is an arbitrary string here; we 
         *   could have used <.reveal xp1-q34r>, except it wouldn't have 
         *   been so meaningul. Had we for example wanted to distinguish Bob 
         *   mentioning the troubles from Sally mentioning the troubles, we 
         *   could use <.reveal bob-troubles> and <.reveal sally-troubles>.
         */        
        '<q>What\'s this town like?</q> you ask.\b
            <q>Oh great. Just great,</q> he replies. Lowering his voice he adds,
            <q>now that the troubles are over.</q><.reveal troubles> ',
                
        'He provides you with a whole lot of information about places to eat,
        shop, visit, and avoid throughout the turn. You don\'t take a tenth of
        it in. '
    ]
    name = 'the town'
;

/*  
 *   ASK TELL TOPIC
 *
 *   Since this is an AskTellTopic it will be used as a response to both ASK 
 *   BOB ABOUT TROUBLES and TELL BOB ABOUT TROUBLES.
 */
++ AskTellTopic, SuggestedAskTopic, StopEventList @tTroubles
    [
        /*  
         *   The first response to ASK ABOUT TROUBLES mentions the 
         *   lighthouse, so once the player has seen this response, the PC 
         *   knows about the lighthouse. We can mark this by using 
         *   gSetKnown(lighthouse). Note that since it's convenient to use 
         *   the <<>> notation within a double-quoted string to do this, we 
         *   once again use the {: } short-form anonymous function to wrap 
         *   the double-quoted string in the EventList.
         */        
        {: "<q>You said something about some troubles,</q> you remark, <q>what
            were they all about?</q>\b
            <q>Well,</q> he begins, <q>it all began up at the lighthouse...</q>
            he breaks off, shaking his head, <q>No, you really don't want to
            know, you really don't -- believe me!</q> <<gSetKnown(lighthouse)>>"
        },
        
        '{The bob/he} steadfastly refuses to tell you anything more about the
        troubles. '
    ]
    
    /* 
     *   Here we test whether the tag 'troubles' has been revealed in order 
     *   to decide whether this TopicEntry is active.
     *
     *   tTroubles is a Topic, not a Thing (see below), so it starts out 
     *   known by default. As an alternative to using <.reveal troubles> and 
     *   testing for gRevealed('troubles') here we could have overridden 
     *   isKnown to nil on tTroubles and then used gSetKnown(tTroubles) to 
     *   make it known once Bob mentioned the troubles. 
     */
    isActive = gRevealed('troubles')
    name = 'the troubles'
;

/*   
 *   Note that tWeather is another Topic (defined below) not a Thing. Using a
 *   lower case initial t to distinguish Topics from Things in this way is 
 *   just a convention I employ, not a requirement; but it's useful to adopt 
 *   some such convetion.
 */
++ AskTellTopic, SuggestedAskTopic @tWeather
    "<q>Nice weather we're having,</q> you remark.\b
    <q>Not bad for the time of year, sir,</q> he agrees. "    
    name = 'the weather'
;

/*   
 *   ASK FOR TOPIC
 *
 *   We use an AskForTopic to handle commands like ASK BOB FOR SOMETHING. 
 *   Since Bob is desribed as stacking cans we'll allow the PC to ask for a 
 *   can; but since it's irrelevant to the plot, we shan't suggest it. 
 */
++ AskForTopic @cans
    topicResponse()
    {
        "<q>Can I have one of those cans please?</q> you ask.\b
        <q>Sure,</q> he replies. He turns round, picks up a can, and then
        turns back and hands it to you. <q>Here you are,</q> he announces. ";
        
        /*  Create a new Can object and then move it to the PC's inventory. */
        local obj = new Can;
        obj.moveInto(me);
    }
;

/*   
 *   ALT TOPIC
 *
 *   We use an AltTopic to provide an alternative response to a 
 *   conversational command when a particular condition (defined in the 
 *   isActive property) holds. We can have as many AltTopics as we like; the 
 *   one that will be used is the last one for which isActive is true.
 *
 *   Here we use AltTopic to put a limit on the number of cans the PC can ask
 *   for, since we don't want the game to generate an infinite number of 
 *   dynamically-created cans. Ten cans should be more than enough, so we'll 
 *   stop when 10 cans have been created.
 */
+++ AltTopic
    "You've had more than enough cans for now. "
    isConversational = nil
    isActive = Can.cansCreated >= 10
;

/*   
 *   ASK TELL GIVE SHOW TOPIC
 *
 *   An AskTellGiveShowTopic provides the same response to ASK BOB ABOUT X, 
 *   TELL BOB ABOUT X, SHOW X TO BOB and GIVE X TO BOB. Here we'll use it for
 *   asking about, telling about, showing or giving cans.
 *
 *   There's one complication, though; the PC can only give Bob a can if the 
 *   PC is holding a can, and since the Can objects given to the player are 
 *   created dynamically, we don't have an object name for any such can. We 
 *   can instead test for an object of kind Can, but a TopicEntry doesn't 
 *   allow us to do that as standard, (hopefully that's something that will 
 *   be changed in some future version of TADS 3.
 *
 *   If it were allowed, we'd simply express the matchObj for this 
 *   TopicEntry as a list:
 */
++ AskTellGiveShowTopic [cans, Can]
    
    /* 
     *   Since the standard library can't handle a class as a matchObj, we 
     *   need to override the matchTopic() method.
     */
    matchTopic(fromActor, topic)
    {
        /* 
         *   Since we know that we've defined matchObj as a list, we know we 
         *   can treat it as a collection, which simplifies what we need to 
         *   put here.
         */
        if (matchObj.indexWhich({x: findMatchObj(x, topic)}) != nil)
                    return matchScore;


        /* didn't find a match - indicate this by returning a nil score */
        return nil;
    }
    
    /*  
     *   We also need to override findMatchObj() so that it can handle both a
     *   Thing (if the player typed GIVE or SHOW) or a ResolvedTopic, and 
     *   can cope with classes as well as individual objects
     */
    
    findMatchObj(obj, rt)
    {
        if(rt.ofKind(obj))
            return true;
        
        if(!rt.ofKind(ResolvedTopic))
            return nil;
        
        /* check the "in scope" list */
        if (rt.inScopeList.indexWhich({x: x.ofKind(obj)}) != nil)
            return true;

        /* check the "likely" list */
        return (rt.likelyList.indexOf({x: x.ofKind(obj)}) != nil);
    }

    topicResponse = "<q>What's in those cans?</q> you ask.\b
        <q>Baked beans,</q> he replies briskly, <q>not just any baked beans,
        though -- the beans in those cans are environmentally-friendly
        anti-carcogenic organic high-fibre baked beans -- the very best!</q> "
;

/*  
 *   DEFAULT ASK TOPIC
 *
 *   The chances are the player will try to ASK Bob about topics for which we
 *   haven't replied a response. We can use a DefaultAskTopic to deal with 
 *   these commands. To avoid making the NPC look like a talking robot, it's 
 *   best to vary such responses, so we also make the DefaultAskTopic a 
 *   ShuffledEventList.
 */

++ DefaultAskTopic, ShuffledEventList
    [
        '{The bob/he} gives a puzzled frown, as if he didn\'t understand your
        question. ',
        
        '<q>Yes, well,</q> he begins, and then gabbles such a rapid reply that
        you don\'t catch a word of it. ',
        
        '<q>The way I see it...</q> he begins; but then the rest of his reply
        is lost in a fit of coughing. '
    ]
;

/*   
 *   DEFAULT TELL TOPIC 
 *
 *   We use DefaultTellTopic to cater for topics we haven't provided a 
 *   specific TELL ABOUT response to.
 */
++ DefaultTellTopic
    "{The bob/he} listens politely as you rattle on. "
;

/*  
 *   COMMAND TOPIC
 *
 *   We can use a CommandTopic to provide a response to an order directed to 
 *   an NPC, such as BOB, GO WEST.
 *
 *   A SystemAction is normally an out-of-game command that can only 
 *   meaningfully be addressed to the parser, such as SAVE, RESTORE, RESTART,
 *   QUIT or UNDO; so we can use a CommandTopic to trap attempts to address 
 *   such commands to an actor and provide a suitable (here suitably 
 *   flippant) response. 
 */
++ CommandTopic @SystemAction
    "<q>Well,</q> he replies, <q>If you ask me that's the sort of command you
    should be addressing to the parser, not to an NPC in the game!</q> "
;

/*  
 *   We can also use a CommandTopic to provide a response to particular 
 *   commands such as BOB, JUMP (as here).
 *
 *   CommandTopic doesn't make it particularly easy to match command 
 *   involving particular objects, such as BOB, TAKE THE RED BALL or BOB, 
 *   PUT THE RED BALL IN THE GREEN BOX; to match such commands you'd need to 
 *   override the matchTopic() method of the CommandTopic. Alternatively you 
 *   could just download and use the TCommand extension.
 */

++ CommandTopic @JumpAction
    "<q>Jump!</q> you cry!\b
    {The bob/he} is so startled that he jumps. "
;

/*  
 *   DEFAULT COMMAND TOPIC
 *
 *   We use a DefaultCommandTopic to handle all orders given to the Bob for 
 *   which we haven't provided a customised response.
 */
++ DefaultCommandTopic
    "<q>Not wishing to be rude, sir, but I don't reckon you have any call to be
    bossing me around,</q> he complains. "
;
     
++ DefaultAskForTopic
    "<q>I'm afraid I don't think I can help you there, sir,</q> he replies. "
;

//==============================================================================
/*  
 *   CODE FOR THE SALLY NPC 
 *
 *   Sally is a more complex NPC than Bob, in that she'll lead the PC to the 
 *   lighthouse and then follow him around. We also use her to illustrate 
 *   ConvNodes and AgendaItems. 
 *
 *   To begin with a summary: Sally starts out in the shop looking for 
 *   clothes, but can't be talked to while she's in that state. As soon as 
 *   she hears Bob mention the troubles she goes outside the shop to wait 
 *   for the PC. When the PC emerges from the shop she offers to take him to 
 *   the lighthouse. If he refuses, the game ends then and there. If he 
 *   accepts she leads him to the lighthouse (she acts as 'TourGuide' so he 
 *   can follow her). Once they arrive at the lighthouse she will follow the 
 *   PC (there's no particular reason for this swap of leading character 
 *   other than to demonstrate the two different ways in which an NPC can be 
 *   made to accompany the PC). Once they start to explore the lighthouse 
 *   together they come face to face with the origin of the troubles.
 *
 *   TOUR GUIDE
 *
 *   We make Sally a TourGuide so that the PC can follow her during the 
 *   appropriate segment of the game.
 */

sally: TourGuide, Person 'petite blonde pretty woman*people' 
    'blonde woman' @shop
    "She's a petite blonde, with a pretty round face. "
    
    properName = 'Sally'
    isHer = true
    globalParamName = 'sally'
    
    uselessToAttackMsg = 'You are not the sort of man that goes round attacking
        defenceless women. '
    
    cannotKissActorMsg = ('You\'d better not; ' + (gRevealed('married') ?
                             'you\'ve already told her you\'re married. ' : 'you
                                 don\'t know her well enough. '))
;

/*  
 *   We add an Unthing to Sally as we did for Bob, to handle attempts to 
 *   refer to Sally before the PC has learned her name.
 */
+ Unthing 'sally' 'Sally'
    'You don\'t know anyone called Sally yet. '
;

/*  
 *   HERMIT ACTOR STATE
 *
 *   A HermitActorState is an ActorState in which an NPC will not respond to 
 *   any conversational commands. It can be used to represent an NPC who is 
 *   preoccupied, stand-offish, unconscious, or even dead. Here we start 
 *   Sally off in a HermitActorState since we don't want the PC to talk to 
 *   her until they meet outside the shop. While he's in the shop the PC 
 *   should concentrate on talking to Bob.
 *
 *   If we mix in an EventList class with an ActorState, the EventList's 
 *   doScript() method will be called every tutn the NPC is in the state, so 
 *   that we can display a list of messages indicating that that NPC is 
 *   actually doing something. Here we make the HermitActorState also a 
 *   ShuffledEventList so that Sally looks a bit livelier than a tailor's 
 *   dummy while she's shopping for clothes. 
 */

+ sallyShopping: HermitActorState, ShuffledEventList
    /* This is the state Sally starts out in. */
    isInitState = true
    
    specialDesc = "{The sally/she} is standing by the clothes rack, carefully
        inspecting everything that's on offer. "
    
    stateDesc = "She's standing by the clothes rack, inspecting what\'s on
        offer. "
    
    /* 
     *   The message to display when we address a conversational command to 
     *   an actor in this state.
     */
    noResponse = "You know better than to interrupt a woman when she's
        hunting for bargains on the clothes rack. "
    
    /*   
     *   The list of random messages to display while the actor is in this 
     *   state.
     */
    eventList = [
        '{The sally/she} takes a coat from the rack, holds it against her body,
        then shakes her head and returns it to the rack. ',
        
        '{The sally/she} continues rifling through the rack. ',
        
        '{The sally/she} picks up a hat and tries it on. She pulls it down over
        her face, frowns, and then puts it back. ',
        
        {: "{The sally/she} takes a <<rand('skirt', 'blouse', 'dress',
                                           'jumper')>> off the rack and mutters
            something disapproving about the wrong shade of <<rand('blue',
                'green', 'yellow', 'orange', 'pink', 'mauve')>>. "
        },
        
        '{The sally/she} struggles to remove a pair of trousers from the rack,
        then decides that they\'re too long, so struggles to put them back
        again. '
    ]
    
    /* 
     *   It would be intrusive to see these messages every turn, so we'll 
     *   start off displaying them on average on two turns out of three.
     */
    eventPercent = 67
    /*   
     *   Once the player has seen all these messages once, it's probably 
     *   best to make them less frequent before they start to seem obviously 
     *   repetitive and tiresome, so we'll reduce the frequency to one turn 
     *   in three once after displaying five messages.
     */
    eventReduceTo = 33
    eventReduceAfter = 5
    
    /*   
     *   When Sally leaves the shop the default message would be "Sally/The 
     *   blonde woman leaves to the north." We replace that here with 
     *   something more appopriate to the context.
     */
    sayDepartingDir(dir, conn)
    {
        "Then she turns away again and hurries out of the shop. ";
    }
;

/*  
 *   ACTOR STATE
 *
 *   So far we've used various subclasses of ActorState, but sometimes it 
 *   can be useful to use ActorState itself when we don't need any of the 
 *   specializations the subclasses provide, as here when the ActorStatre is 
 *   used simply while Sally is waiting outside the shop.
 */
+ sallyStreetState: ActorState
    specialDesc = "{The sally/she} is standing just outside the shop. "
;

/*  
 *   GUIDED TOUR STATE
 *
 *   As soon as the PC emerges from the shop Sally will offer to take him to 
 *   the lighthouse. A GuidedTourState allows us to set up a mechanism 
 *   whereby the PC can follow the NPC. We need to define a series of 
 *   GuidedTourStates, each one representing one step of the journey.
 */

+ sallyTour1State: GuidedTourState
    commonDesc = 'waiting for you to follow her down the street to the east'
    stateDesc = "She\'s <<commonDesc>>. "
    specialDesc = "{The sally/she} is <<commonDesc>>. "
    
    /* 
     *   The next state Sally will go to once the PC has followed her for one
     *   step in this state.
     */
    stateAfterEscort = sallyTour2State
    
    /*   The TravelConnector through which Sally wants to lead the PC */
    escortDest = street.east
    
    /*   
     *   The GuidedInTravelState to use when the PC follows Sally. Sally will
     *   switch to this state temporarily while travel is in progress, and 
     *   it will be used to describe Sally's part in the travel (e.g. "Sally 
     *   leads the way"). The library provides a perfectly acceptable 
     *   plain-vanilla default, but better results can be obtained by 
     *   customising it, so we'll provide our own class (defined below).
     */
    escortStateClass = SallyEscortState
;

+ sallyTour2State: GuidedTourState
    commonDesc = 'waiting for you to follow her south across the field'
    stateDesc = "She\'s <<commonDesc>>. "
    specialDesc = "{The sally/she} is <<commonDesc>>. "
    stateAfterEscort = sallyTour3State
    escortDest = road.south
    
    /* 
     *   The mesage that's displayed when we've just completed the travel 
     *   that brought us to this point. If the PC follows Sally while she's 
     *   in SallyTour1State (the previous GuidedTourState), this is the 
     *   message that will be shown when Sally and the PC arrive at the 
     *   destination of that first step of the travel.
     */
    arrivingWithDesc = "{The sally/she} comes to a halt on the roadside and
        points southwards across the open field. <q>We'll go that way,</q>
        she says, <q>It's twice as far by road.</q> "
    escortStateClass = SallyEscortState
    
    
    /*  
     *   By default nothing compels the PC to follow the NPC when she's in a 
     *   GuidedTourState; the PC can wander off in any direction he likes, 
     *   and the NPC will be left patiently waiting for him to return to 
     *   her. But if we like we can make the NPC prevent the PC from going 
     *   off in any direction other than the one she wants him to go by 
     *   having her block attempts to go anywhere else. The best place to do 
     *   that is in beforeTravel().
     */
    beforeTravel(traveler, connector)
    {
        if(connector == road.east)
        {
            "<q>No, it's three times the distance that way!</q> she stops you, 
            <q>we should take the short cut across the field -- it's much
            quicker!</q>";
            exit;
        }
        if(connector == road.west)
        {
            "<q>You can't be giving up on me already!</q> she protests, <q>It's
            <i>that</i> way,</q> she adds, pointing firmly south across the
            field. ";
            exit;
        }
        
        /* 
         *   We must call the inherited handling here, or the whole 
         *   GuidedTour mechanism will be broken.
         */
        inherited(traveler, connector);
    }
;

+ sallyTour3State: GuidedTourState
    commonDesc = 'waiting for you to follow her down the hill to the southeast'
    stateDesc = "She\'s <<commonDesc>>. "
    specialDesc = "{The sally/she} is <<commonDesc>>. "
    stateAfterEscort = sallyArriving
    escortDest = hillTop.southeast
    escortStateClass = SallyEscortState
    arrivingWithDesc = "As you arrive at the top of the hill {the sally/she}
        pauses and points down to the southeast. <q>That way,</q> she announces.
        "
;

/*   
 *   GUIDED IN TRAVEL STATE 
 *
 *   The GuidedInTravelState is the complement to the GuidedTourState. It's 
 *   used every time the NPC actually leads the PC on a step of the way, the 
 *   main purpose being to display a message describing the NPC leading the 
 *   PC.
 */
class SallyEscortState: GuidedInTravelState
    /* 
     *   It's the sayDeparting() method that actually displays the message 
     *   describing the NPC leading the PC.
     */
    sayDeparting(conn)
    {
        /* 
         *   This is where we use the custom followDesc property we defined 
         *   when setting up the map. If the connector through which Sally 
         *   and the PC are travelling defines the followDesc property, we 
         *   use it to construct the message describing the travel. 
         *   Otherwise we use the plain-vanilla message inherited from the 
         *   standard library. Customising the message on a per-connector 
         *   basis like this makes the output read quite a lot better.
         */
        if(conn.propDefined(&followDesc))
            "{The sally/she} leads the way <<conn.followDesc>>. ";
        else
            inherited(conn);
    }
;

/*   
 *   ACTOR STATE
 *
 *   Sally will leave this ActorState almost as soon as she enters it. It's 
 *   purpose is to smooth the transition from Sally leading the PC to Sally 
 *   following the PC.
 */

+ sallyArriving: ActorState
    arrivingTurn()    
    {
        /* 
         *   As soon as the PC and Sally arrive together at the lighthouse, 
         *   we switch her to the sallyFollowing state (in which she will 
         *   follow the PC wherever he leads) and make her initiate a 
         *   conversation using the 'lighthouse' Conversation Node (for 
         *   which see below). The following statement achieves both of 
         *   these things at once.
         */
        sally.initiateConversation(sallyFollowing, 'lighthouse');
    }
    /*  
     *   The message to display indicating that Sally has stopped acting as 
     *   lead actor.
     */
    arrivingWithDesc = "{The sally/she} comes to a stop outside the old
        lighthouse and turns to face you. "
;

/*  
 *   ACCOMPANYING STATE
 *
 *   An Actor in an AccompanyingState will follow the PC around for as long 
 *   as she's in that state (although that behaviour can be overridden to 
 *   prevent the NPC from following the PC through certain connectors if 
 *   desired. Once she enters this state, Sally will remain in it for the 
 *   rest of the game. 
 */

+ sallyFollowing: AccompanyingState
    specialDesc = "{The sally/she} is at your side. "
    
    /* 
     *   The arrivingTurn() method is called each time the NPC arrives in a 
     *   at a location while following the PC. Here we use it to make Sally 
     *   say or do something via an InitiateTopic keyed on the location just 
     *   arrived at; calling sally.initiateTopic(obj) will cause the 
     *   corresponding InitiateTopic @obj to be triggered.
     */
    arrivingTurn()
    {
        sally.initiateTopic(sally.location);
    }
    
    /*  
     *   Normally an NPC will follow the PC anywhere, but we can change that 
     *   by overriding accompanyTravel(). Here we'll stop Sally following 
     *   the PC back into town. This doesn't stop the PC returning to the 
     *   town; Sally simply won't follow him there. If we wanted to prevent 
     *   the PC returning to the town as well while Sally's following for 
     *   him we could also do that in this method, by checking for 
     *   conn==road.west and then displaying a message and using the exit 
     *   macro to prevent travel, e.g.
     *
     *.  accompanyTravel(traveler, conn)
     *.  {
     *.    if(conn == road.west)
     *.    {
     *.       "<q>Let's not go that way,</q> {the sally/she} urges. ";
     *.        exit;
     *.    }
     *.    return true;
     *.  }
     *
     */
    accompanyTravel(traveler, conn) 
    { 
        return conn != road.west; 
    }

    /*  
     *   Just as a GuidedTourState uses a GuidedInTravelState to describe the
     *   actual travel, so an AccompanyingState uses an associated 
     *   AccompanyingInTravelState. We override getAccompanyingTravelState 
     *   here to use a custom AccompanyingInTravelState.
     */    
    getAccompanyingTravelState(traveler, connector)
    {
        /* 
         *   Create the default intermediate state for the travel.  Note
         *   that the lead actor is the actor performing the command - this
         *   won't necessarily be the traveler, since the actor could be
         *   steering a vehicle.  
         */
        return new SallyAccompanyingInTravelState(
            getActor(), gActor, getActor().curState);
    }
;

/*   
 *   ACCOMPANYING IN TRAVEL STATE 
 *
 *   While the library provides a perfectly acceptable plain-vanilla message 
 *   to describe an NPC following the PC around ("Sally comes with you") we 
 *   can get better results by using a custom AccompanyingInTravelState.
 */  

class SallyAccompanyingInTravelState: AccompanyingInTravelState
    /* 
     *   Once again we define sayDeparting() so that it uses the followDesc 
     *   of the connector being traversed to construct a message specific to 
     *   that connector (such as "Sally follows you through the door").
     */
    sayDeparting(conn)
    {
        if(conn.propDefined(&followDesc))
            "{The sally/she} follows you <<conn.followDesc>>. ";
        else
            inherited(conn);
    }
;

/*  
 *   INITIATE TOPIC
 *
 *   An InitiateTopic is triggered by an explicit call to 
 *   actor.initiateTopic. It's use to make an NPC react to specific things 
 *   in the game. These InitiateTopics are all triggered from the 
 *   arrivingTurn() method of sallyFollowing to make Sally respond to 
 *   arriving in various locations. Note that we're also locating all these 
 *   InitiateTopics in the sallyFollowingState (the definition of the 
 *   SallyAccompanyingInTravelState forms no part of the object containment 
 *   hierarchy).
 */
++ InitiateTopic @lobby
    topicResponse()
    {
        "<q>Well, this is it,</q> {the sally/she} announces, <q>Let\'s take a
        look around.</q> ";
        
        /*  
         *   This is the way to add a DelayedAgendaItem (for which see 
         *   below) to Sally's agenda list.
         *
         */
        sally.addToAgenda(sallyImpatientAgenda.setDelay(4));
        /* 
         *   We only want this InitiateTopic to fire on the first time Sally 
         *   enters the lobby. Setting isActive to nil here disables this 
         *   InitiateTopic for subsequent occasions.
         */
        isActive = nil;
    }
;

++ InitiateTopic @midStair
    "<q>That's it,</q> {the sally/she} announces, pointing to the oak door,
    <q>the answer to all your questions lies through there. Enter if you
    dare!</q> "
;

++ InitiateTopic @horrorChamber
    topicResponse()
    {
        "She lets out a piercing scream; but you're only dimly aware of
        her presence; as your stomach threatens to empty its contents, the room
        seems to start spinning around you, and then everything goes blank.\b";
        
        /*  
         *   We'll end the game at this point (it's only a demo, after all!).
         *   Note how we can use finishGameMsg to display a custom finishing
         *   message.
         */
        finishGameMsg('YOU HAVE FAINTED', [finishOptionUndo]);
    }
;

++ InitiateTopic @road
    "<q>I'm not going back into town until I've shown you what's in the
    lighthouse,</q> {the sally/she} warns you. "
;

++ InitiateTopic, StopEventList @store
    [
        '<q>They took everything out after the troubles,</q> {the sally/she}
        remarks, <q>there\'s nothing interesting in this part of the lighthouse
        now.</q> ',
        
        '<q>Like I said, there\'s nothing interesting left in this part of the
        lighthouse now,</q> {the sally/she} remarks, <q>let\'s look somewhere
        else.</q> '
    ]
;


++ InitiateTopic, StopEventList @office
    [
        '<q>There doesn\'t seem to be much here, does there?</q>
        {the sally/she} declares. ',
        
        '{The sally/she} glances around the room without much interest. '
    ]
;

++ InitiateTopic @cliffPath
    "<q>Well, there was a <i>lot</i> of point in coming here, wasn\'t there!</q>
    {the sally/she} declares. "
;

/*   
 *   TOPIC GROUP
 *
 *   We'll define a few conversational responses for Sally; these could all 
 *   go directly in the sally object, but we can also use a TopicGroup for 
 *   the purpose (and since there are so many other kinds of thing going 
 *   directly in the actor, using a TopicGroup can seem neater).
 */

+ TopicGroup
    /* 
     *   The TopicEntries in this TopicGroup will only be reachable when this
     *   isActive condition is true. As a matter of fact it will true 
     *   practically all the time the PC is in a position to talk to Sally in
     *   any case (since GuidedTourState is a subclass of 
     *   AccompanyingState), so we're just illustrating the principle here. 
     *   In a real game you might want to use a more restrictive condition.
     */
    isActive = (sally.curState.ofKind(AccompanyingState))
;

++ AskTopic, SuggestedAskTopic, StopEventList @sally
    [
        '<q>Are you married, Sally?</q> you ask.\b
        <q>I was,</q> she tells you, <q>but then the troubles came. But how
        about you? Do you have a family?</q><.convnode family> ',
        
        '<q>How did the troubles...?</q> you begin.\b
        <q>End my marriage?</q> she asks, <q>Well, it\'s a long story. Maybe
        I\'ll tell you about it some other time.</q> ',
        
        '<q>Is there a man in your life now?</q> you ask.\b
        <q>I can but keep hoping,</q> she smiles coyly. ',
        
        '<q>Tell me more about yourself, Sally,</q> you say.\b
        <q>Some other time,</q> she replies. '
    ]
    name = 'herself'
    timesToSuggest = 3
;

+++ AltTopic, SuggestedAskTopic
    "<q>I'm sorry, I don't think we've been introduced,</q> you say, <q>You
    are...?</q>\b
    <q>I'm <<sally.makeProper()>>,</q> she tells you, <q>I've lived round here
    most of my life.</q>"
    name = 'herself'
    isActive = !sally.isProperName
;

   /* 
    *   ASK TELL TOPIC
    *
    *   Note that as an alternative to using the @obj template syntax to 
    *   make a TopicEntry match the single object obj, we can provide a list 
    *   of objects that a TopicEntry can match. Here we make asking/telling 
    *   Sally about the lighthouse equivalent to asking/telling her about 
    *   the troubles (or about the oakDoor once the PC has seen it)
    */
++ AskTellTopic, SuggestedAskTopic [lighthouse, tTroubles, oakDoor]
    "<q>So what were the troubles, and what are we going to find at the
    lighthouse?</q> you ask.\b
    <q>You'll see when we get there,</q> she assures you. "
    name = 'the lighthouse'
;

/*   
 *   ALT TOPIC
 *
 *   We also provide a whole series of AltTopics to tailor the response to 
 *   ASK/TELL SALLY ABOUT LIGHTHOUSE/TROUBLES according to the current 
 *   situation.
 */
+++ AltTopic, SuggestedAskTopic
    "<q>So, what did happen in the lighthouse?</q> you ask, <q>is that where the
    troubles started?</q>\b
    <q>The answers are all in the lighthouse,</q> she tells you, <q>if you want
    to find out what happened, you'll have to go inside.</q> "
    name = 'the lighthouse'
    isActive = me.hasSeen(lighthouse)
;

+++ AltTopic, SuggestedAskTopic
    "<q>So, what did happen here?</q> you want to know, <q>And what were the
    troubles?</q>\b
    <q>Keep looking,</q> she tells you, <q>you'll see soon enough!</q> "
    name = 'the troubles'
    isActive = me.location is in (lobby, store, office)
;

+++ AltTopic, SuggestedAskTopic
    "<q>What's on the other side of that door?</q> you ask, <q>is that where
    the troubles started?</q>\b
    <q>If you go through it you'll see,</q> she tells you. "    
    name = 'the troubles'
    isActive = me.isIn(midStair)
;

++ TellTopic, StopEventList @me
    [
        '<q>I\'m new to this area,</q> you tell her.\n
        <q>So I gathered,</q> she remarks. ',
        
        'You carry on telling her about yourself, but you sense she isn\'t
        really listening. '
    ]
;

++ CommandTopic @SystemAction
    "<q>What a very curious request!</q> she declares. "
;

++ DefaultCommandTopic
    "<q>My husband used to try telling me what to do all the time, too;</q> she
    remarks, <q>frankly, it's not something I find attractive in a man.</q> "
;

/*   
 *   DEFAULT GIVE SHOW TOPIC
 *
 *   We can use a DefaultGiveShowTopic to provide a response to attempt to 
 *   Give or Show anything to Sally (for which we haven't provided any more 
 *   specific handling).
 */

++ DefaultGiveShowTopic
    "<q>Yes, {it\'s dobj/he\'s} {a dobj/he},</q> she observes, <q>very
    interesting, I\'m sure!</q> "
;

/*   
 *   DEFAULT ANY TOPIC
 *
 *   A DefaultAnyTopic will match anything for which we haven't provided a 
 *   more specific response. Here, for example, it would match ASK SALLY 
 *   ABOUT HER MOTHER, or TELL SALLY ABOUT CHINESE COOKERY. A 
 *   DefaultAnyTopic is used as a last resort: any other kind of 
 *   DefaultXXXXTopic (e.g. DefaultAskTopic, DefaultTellTopic, 
 *   DefaultGiveShowTopic) will be used in preference to it (so our 
 *   DefaultGiveShowTopic will still work, but the DefaultAnyTopic would 
 *   have handled GIVE & SHOW if we hadn't defined a DefaultGiveShowTopic.
 *
 *   Here we use a DefaultAnyTopic in place of defining separate a 
 *   DefaultAskTopic and DefaultTellTopic (given that most other kinds have 
 *   already been covered). We could have done almost the same job in this 
 *   particular context with a DefaultAskTellTopic.
 */

++ DefaultAnyTopic
    "<q>We can talk later; let's just get to the lighthouse,</q> she replies. "
;

/*   
 *   ALT TOPIC
 *
 *   We can use AltTopics with DefaultTopics as well as other kinds of 
 *   TopicEntry. Normally one would use a ShuffledEventList to vary the 
 *   response to a DefaultTopic, but here we'll use a whole series of 
 *   AltTopics to vary the response according to the situation.
 */
+++ AltTopic
    "<q>Let\'s talk about that some other time,</q> she suggests, <q>I think we
    should go back to the lighthouse before it gets dark.</q> "
    isActive = me.hasSeen(lighthouse) && !me.canSee(lighthouse)
;

+++ AltTopic
    "<q>All this talk!</q> she complains, <q>anyone would think you were afraid
    to go in there!</q> She nods towards the lighthouse. "
    isActive = me.isIn(cliff)
;

+++ AltTopic, ShuffledEventList
    [
        '<q>This doesn\'t seem the right place to talk about that,</q> she
        replies, <q>I thought you were anxious to find out about the
        troubles!</q> ',
        
        '<q>Later,</q> she urges you, <q>in any case, you\'ll see everything
        differently once you\'ve seen the source of the troubles.</q> ',
        
        '<q>We can discuss that some other time,</q> she tells you, <q>right
        now, let\'s concentrate on what we came for!</q> '
    ]
    isActive = me.location is in (lobby, store, office, midStair)
;


//------------------------------------------------------------------------------
/*   
 *   CONVERSATION NODES
 *
 *   A Conversation Node, or ConvNode (as the class is actually called) 
 *   represents a particular point in a conversation at which a particular 
 *   set of responses becomes appropriate. For example, if an NPC asks a 
 *   question, it may be pertinent to reply YES or NO immediately after, 
 *   whereas interjecting YES or NO into the conversation at some later or 
 *   earlier point of the conversation would either be meaningless, or else 
 *   would have some quite different meaning. 
 *
 *
 *   There are basically two types of Conversation Node that one might 
 *   typically be implemented in TADS 3, which one might for convenience 
 *   called 'open' and 'closed' (though this is nowhere defined in the 
 *   library's nomenclature). An 'open' node is one at which a particular 
 *   set of responses becomes appropriate (such as YES or NO or some more 
 *   detailed reply), but the player doesn't have to use one of them; the 
 *   player can take the opportunity to use one of these responses or ignore 
 *   it (though once ignored, the opportunity is lost, as it would be in a 
 *   real conversation).
 *
 *   A 'closed' node, on the other hand, is where the NPC demands an answer 
 *   and won't let the conversation move on until s/he receives one. This is 
 *   the more laborious kind of Conversation Node to implement, but it's the 
 *   one we'll illustrate first, as it demonstrates more of the features of 
 *   ConvNodes, and once we've implemented a 'closed' node the 'open' ones 
 *   will seem easy!
 *
 *   The ConvNode below is used for Sally to offer to show the PC to the 
 *   lighthouse; she'll demand that he says yes or no.
 */

+ ConvNode 'troubles'
    /* 
     *   The message to display when this ConvNode is activated through a 
     *   call to initiateConversation(). The message should be used for the 
     *   NPC to ask the question (or make the statement) demanding an answer 
     *   from among the options available in this ConvNode.
     *
     *   If this ConvNode was going to be used several times we might want to
     *   vary the message displayed here, in which case we could use 
     *   npcGreetingList instead of npcGreetingMsg, but we'll only ever enter
     *   this ConvNode once, so npcGreetingMsg will so just fine.
     */
    npcGreetingMsg = "{The sally/she} walks up to you and says, <q>Excuse me,
        but I couldn't help hearing you asking about the troubles. Would you
        like me to show you the lighthouse?</q> "
    
    /*
     *   A set of messages to display in which the NPC reminds the PC that 
     *   she's waiting for an answer if he hasn't spoken to her on that 
     *   turn. If we were content to use the same message each time we could 
     *   use npcContinueMsg instead, but as that could make Sally seem rather
     *   robotic it's better to vary her nags with a ShuffledEventList on 
     *   npcContinueList instead.
     */    
    npcContinueList: ShuffledEventList
    {
        [
            '<q>The lighthouse,</q> {the sally/she} reminds you, <q>I offered
            to take you to see it. Would you like me to?</q> ',
            
            '<q>I\'m still waiting to hear if you\'d me to show you the
            lighthouse,</q> {the sally/she} reminds you. ',
            
            '<q>I\'ve been kind enough to offer to show you the lighthouse, so
            I think I deserve an answer,</q> {the sally/she} remarks, <q>So --
            are you coming?</q> '
        ]
        /*  
         *   Sally would seem needlessly importunate if she nagged the PC 
         *   for a response on every turn he didn't say something, so we'll 
         *   use one of these messages on average only every other turn. 
         */
        eventPercent = 50
    }
    
    /*  
     *   Since Sally is insisting on an answer, we need to block the various 
     *   ways in which the player could simply end the conversation. Walking 
     *   away from her might be one way; saying goodbye would be another. We 
     *   use canEndConversation() to block both.
     */
    canEndConversation(actor, reason)
    {
        if(reason == endConvTravel)
            "<q>Don't walk away when I'm talking with you!</q> {the sally/she}
            protests, <q>I asked you a question: do you want me to show you the
            lighthouse or don't you?</q> ";
        if(reason == endConvBye)
            "<q>Don't you <q>goodbye</q> me when I ask you a question!</q> {the
            sally/she} storms, <q>Now, are you coming with me to the lighthouse
            or aren't you?</q> ";
        
        /*  
         *   blockEndConv is a special value which not only doesn't allow the
         *   conversation to be ended while we're in this ConvNode but also 
         *   tells the caller that we've displayed a message explaining why, 
         *   so that we don't also need to display one of the messages from 
         *   npcContinueList/
         */
        return blockEndConv;
    }
    
    /*  
     *   There's no way the compiler can readily tell if this is an 'open' 
     *   ConvNode or a 'closed' one. If it were 'open' the player could also 
     *   reach TopicEntries defined outside this ConvNode, so we'd want them 
     *   to be suggested too. In this case we've made it a 'closed' ConvNode 
     *   by adding a DefaultAnyTopic (see below) which keeps Sally in this 
     *   ConvNode until we explicitly say otherwise.
     */    
    limitSuggestions = true
    
    /*   
     *   Normally, we'd leave a ConvNode as soon as the player entered a 
     *   conversational command. That's the behaviour we'd want for an 'open'
     *   node. But since this is a closed one we don't want a conversational
     *   command to make us leave this ConvNode unless we explicitly say so,
     *   so we make this ConvNode sticky.
     */
    isSticky = true
;

/*  
 *   YES TOPIC
 *
 *   A YesTopic handles the response when the player types YES. Normally a 
 *   YesTopic will only be useful in a ConvNode (to handle a reply to a 
 *   question the NPC has just asked. Making it a SuggestedYesTopic also 
 *   means that 'say yes' will be one of the suggestions offered to the 
 *   player/
 */
++ YesTopic, SuggestedYesTopic
    topicResponse()
    {
        /*  
         *   If the player replies YES, we want to leave this ConvNode. 
         *   Since the ConvNode is sticky we can only do that by changing to 
         *   another ConvNode. There's no ConvNode called 'nil' so the tag 
         *   <.convnode nil> takes us out of this ConvNode without 
         *   activating another one. Using any non-existent node name would 
         *   have done just as well, but 'nil' makes it clear what we're 
         *   doing -- provided we never use 'nil' as a ConvNode name!
         */
        "<q>Yes, all right, take me to the lighthouse,</q> you say.\b
        <q>Follow me,</q> she replies, starting towards the east. <.convnode
        nil>";
        
        /*   
         *   If the player says YES, Sally will lead him to the lighthouse, 
         *   so we need to put her in the first of her GuidedTourStates.
         */
        sally.setCurState(sallyTour1State);
    }
;

/*   
 *   NO TOPIC
 *
 *   A NoTopic is just like a YesTopic, except that it handles a response to 
 *   NO.
 */
++ NoTopic, SuggestedNoTopic
    topicResponse()
    {
        "<q>No, I'm a bit busy right now,</q> you say.\b
        <q>Very well, suit yourself,</q> she shrugs, <q>It's your loss, not
        mine,</q><.convnode nil>\b
        So saying, she turns away; and with her goes your only chance of
        learning about the lighthouse and the troubles.\b";
        
        /* 
         *   If the PC refuses to follow Sally, then the plot (such as it 
         *   is) has been derailed, so we may as well end the game. We do so 
         *   with a suitable custom message.
         */        
        finishGameMsg('YOU ARE FEEBLY FAINT-HEARTED', [finishOptionUndo]);
        
    }
;

/*  
 *   SPECIAL TOPIC
 *
 *   A SpecialTopic is a special kind of TopicEntry that can only be used 
 *   inside a ConvNode. It allows the player to respond with things that 
 *   normally wouldn't be possible in the standard ASK/TELL system. The 
 *   SpecialTopic we define immediately below will be suggested to the 
 *   player as "as why she's offering."
 */
++ SpecialTopic, StopEventList 'ask why she\'s offering'
    /* 
     *   The list of keywords that can be used to trigger this SpecialTopic. 
     *   We need to make sure that the exact phrasing of the suggestion 
     *   works, but we also need to bear in mind that more players than not 
     *   will insist on trying some other phrasing and consider it a bug 
     *   when it doesn't work, so we need to cater for the obvious variants 
     *   as well. The SpecialTopic will be matched so long as all the words 
     *   entered by the player are in this list of keywords, so this will 
     *   match ASK HER WHY or WHY ARE YOU OFFERING (for example) as well as 
     *   the phrasing actually suggested.
     */
    ['ask', 'her', 'why', 'she', 'she\'s', 'offering', 'are', 'you']
    [
        '<q>Why are you offering to show me?</q> you ask.\b
        <q>Because you\'re new here; you need to understand what happened,</q>
        she replies. <q>So, are you coming? Yes or no?</q>',
        
        '<q>I still don\'t understand why you\'re so keen to show me the
        lighthouse,</q> you say.\b
        <q>Like I said, you need to understand what happened there. So, do you
        want me to show you?</q> '
        
    ]
;
    
/* 
 *   If the PC doesn't yet know who Sally is (he could have found out from 
 *   Bob), we'll let him ask her.
 */
++ AskTopic, SuggestedAskTopic @sally
    "<q>Who are you?</q> you ask.\b
    <q>I'm <<sally.makeProper()>>,</q> she replies briskly, <q>and I've been
    around here long enough to know a thing or two. So, do you want me to take
    you to the lighthouse?</q> "
    isActive = (!sally.isProperName)
    name = 'herself'
;

/*   
 *   DEFAULT ANY TOPIC
 *
 *   This DefaultAnyTopic ensures that this ConvNode remains 'closed', since 
 *   it will be triggered in response to any conversational command except 
 *   for the four explicitly handled above. Without one or more 
 *   DefaultTopics to trap other conversational commands, they would be 
 *   handled by the next TopicDatabase (ActorState or Actor) in the 
 *   hierarchy, and we'd slip straight out of the ConvNode.
 *
 *   In a real game we'd probably want to make the DefaultAnyTopic a 
 *   ShuffledEventList to vary the responses.
 */ 
++ DefaultAnyTopic
    "<q>Never mind that,</q> she says, <q>I asked you if you wanted me to show
    you the lighthouse. Do you?</q> "
;
    
//------------------------------------------------------------------------------
/*   
 *   AN 'OPEN' CONVERSATION NODE 
 *
 *   This is the conversation node that's triggered when Sally and the PC 
 *   arrive outside the lighthouse. She asks the PC whether they should go 
 *   inside, but that's an invitation to action rather than a demand for a 
 *   verbal response, so though we should cater for a verbal response, we 
 *   shouldn't demand one. So we can use the much simpler 'open' ConvNode 
 *   coding structure here.
 */
+ ConvNode 'lighthouse'
    /* The message to display when Sally enters this ConvNode. */
    npcGreetingMsg = "<q>Well, here we are then,</q> she tells you, <q>This is
       where it all started. Shall we go inside?</q>"    
;

++ YesTopic, SuggestedYesTopic
    "<q>Yes; that's what came for, isn't it?</q> you reply.\b
    <q>Quite,</q> she agrees, <q>So lead on!</q> "    
;

++ NoTopic, SuggestedNoTopic
    "<q>No, I don't like the look of it,</q> you reply.\b
    <q>Don't be a ninny!</q> she protests, <q>It's quite safe! So -- lead
    on!</q>"
;

++ AskTopic, SuggestedAskTopic @lighthouse
    "<q>What will we find in there?</q> you ask.\b
    <q>Why don't we go in and see?</q> she replies, <q>Shall we?</q> <.convstay>
    "
    name = 'the lighthouse'
;

/* 
 *   There's no DefaultTopic at the end of this ConvNode structure. If the 
 *   player responds with anything other than YES, NO or ASK ABOUT 
 *   LIGHTHOUSE, we'll simply fall straight out of the ConvNode, since we'll 
 *   have left the point of the conversation at which any of these responses 
 *   made sense.
 */


//------------------------------------------------------------------------------
/*   
 *   A THREADED CONVERSATION NODE CHAIN
 *
 *   We can also use ConvNodes to created a threaded conversational chain. 
 *   This could be done with 'closed' nodes, but quickly becomes very 
 *   laborious (perhaps for the player as well as the author!). Here we just 
 *   illustrate a chain of very simple 'open' nodes. Note that the 
 *   conversation will only continue on to the next node in the chain if the 
 *   player gives the 'correct' answer on every turn (though we could have 
 *   programmed a branching chain had we wanted to). In this case this is a 
 *   reasonable model of how such a conversation might proceed.
 */

+ ConvNode 'family'
    /* 
     *   Note that we don't have to define any other properties of this 
     *   ConvNode. Sally has already asked the question that triggered this 
     *   ConvNode so we don't even need to supply an npcGreetingMsg.
     */
;

++ YesTopic, SuggestedYesTopic
    "<q>Yes, I've just moved here with my wife and kids,</q> you say.\b
    <q>Well, I hope you like it here!</q> she replies.<.reveal married> "
;

++ NoTopic, SuggestedNoTopic
    /* 
     *   The tag <.convnode drink> at the end of this reply will take the 
     *   Conversation to the next ConvNode (called 'drink').
     */
    "<q>No, I'm a bachelor,</q> you reply.\b
    <q>Really?</q> she replies, <q>I'd've thought a guy like you -- well, maybe
    we could have a drink some time, and you can tell me all about it.</q>
    <.convnode drink> "
;

+ ConvNode 'drink'
;

++ YesTopic, SuggestedYesTopic
    "<q>Yes, I'd like that,</q> you say.\b
    <q>Good!</q> she smiles, <q>How about tonight? I know a great little place
    we could go, so, if you're free...</q> <.convnode date>"
;

++ NoTopic, SuggestedNoTopic
    "<q>No, I'd better not. My girlfriend probably wouldn't understand,</q> you
    tell her.\b
    <q>Oh, very well then,</q> she replies. "
;

+ ConvNode 'date'
;

++ YesTopic, SuggestedYesTopic
    "<q>Yes, I'm free tonight,</q> you say, <q>let's meet up for a drink
    then!</q>\b
    <q>It's a date!</q> she declares with a broad grin. "
;

++ NoTopic, SuggestedNoTopic
    "<q>No, I'm busy tonight,</q> you reply, <q>just moved it, so many things to
    attend to...</q>\b
    <q>Some other time then, maybe,</q> she suggests. <.convnode other>"    
;

+ ConvNode 'other'
;

++ SpecialTopic 'suggest tomorrow' ['suggest', 'how', 'about', 'tomorrow']
    "<q>How about tomorrow?</q> you suggest.\b
    <q>That would be great -- it's a date!</q> she beams. "
;

++ SpecialTopic 'answer vaguely' ['answer', 'vaguely', 'be', 'vague']
    "<q>Yes, some other time,</q> you agree. "
;


//==============================================================================
/*  
 *   AGENDA ITEMS
 *
 *   AgendaItems provide a mechanism to allow NPCs to pursue goal-directed 
 *   behaviour or responded to particular events. Each NPC has an agendaList 
 *   (which may, of course, be empty at any one time), and on each turn when 
 *   the NPC is not otherwise engaged his/her/its agendaList is searched for 
 *   an AgendaItem that's ready to be used. If one is found, it's 
 *   invokeItem() method is called. 
 *
 *   We want Sally to walk out of the shop and wait for the PC as soon as she
 *   hears Bob mention the lighthouse. An AgendaItem is ideal for this.
 */

+ AgendaItem
    /* 
     *   AgendaItems normally have to be added to an NPC's AgendaList 
     *   explicitly, but by setting this property to true we can have an 
     *   AgendaItem automatically added to the agendaList at the start of 
     *   the game, which is what we want here.
     */
    initiallyActive = true
    
    /*   
     *   An AgendaItem can fire once its isReady property is true. The PC 
     *   learns about the lighthouse when Bob first mentions it, so checking 
     *   for when the PC knows about the lighthouse should make this 
     *   AgendaItem fire at the right time.
     */
    isReady = (me.knowsAbout(lighthouse))
    
    /*   The code to execute once this AgendaItem is triggered. */
    invokeItem
    {
        /*  
         *   Note that text displayed in the invokeItem() of an AgendaItem 
         *   will only be displayed if the PC can see the NPC in question. 
         *   Normally this is what we want, since it allows us to describe 
         *   what an NPC is doing without worrying if the PC is there to see 
         *   it or not (if the PC isn't, the text won't be displayed), but 
         *   occasionally this can catch us out, not least if the 
         *   invokeItem() method moves the NPC in or out of the PC's scope 
         *   in the course of execution.
         *
         *   Here, though, it's straightforward, since the PC will always be 
         *   present to see Sally's reaction to the mention of the lighthouse.
         */
        "{The sally/she} pauses from her clothes-hunting and throws you an
        anxious glance. ";
        
        /*  Make Sally leave the shop. */
        sally.scriptedTravelTo(street);
        
        /*  Change Sally's ActorState to sallyStreetState */
        sally.setCurState(sallyStreetState);
        
        /*  Add a new AgendaItem to Sally's agendaList. */
        sally.addToAgenda(sallyStreetAgenda);
        
        /*  
         *   We're done with this AgendaItem, so it can be removed from 
         *   Sally's agendaList; we show this by setting isDone = true. If 
         *   we didn't this AgendaItem would keep on firing while its 
         *   isReady property evaluated to true. 
         *
         *   In come cases we might want an AgendaItem to be triggered over 
         *   several terms, in which case we wouldn't set isDone = true 
         *   until we were done with it; but this AgendaItem is a one-off, 
         *   so we set isDone = true first time through.
         */
        isDone = true;
    }
;

/*   
 *   CONV AGENDA ITEM
 *
 *   A ConvAgendaItem is used to allow the NPC to try to say something on 
 *   his or her own initiative. By default it becomes ready when (a) the NPC 
 *   is in a position to talk to the PC and (b) the PC hasn't conversed with 
 *   the NPC on that turn. When those two conditions are met the NPC can 
 *   inject her conversational gambit into the conversation. 
 *
 *   We want Sally to offer to lead the PC to the lighthouse as soon as he 
 *   emerges from the shop. A ConvAgenaItem will do this job for us, since 
 *   the Sally can't talk to the PC when he's inside the shop and she's 
 *   outside, but will be able to as soon as he comes out of the shop. 
 */

+ sallyStreetAgenda: ConvAgendaItem
    invokeItem()
    {                
        /* 
         *   Initiate a conversation using the 'troubles' ConvNode without 
         *   changing the current ActorState.
         */
        sally.initiateConversation(nil, 'troubles');
        
        /* This is a once-off AgendaItem, so note we're done with it. */
        isDone = true;
    }
;

/*
 *   DELAYED AGENDA ITEM
 *
 *   A DelayedAgendaItem is one that fires a predetermined number of turns 
 *   after its added to its actor's agendaList.
 *
 *   If the PC wanders around for too long after entering the lighthouse 
 *   without going to the room Sally wants him to visit, she'll become 
 *   impatient enough to give him a verbal nudge. Since this is a 
 *   conversational activity, it's appropriate to make this a ConvAgendaItem 
 *   as well. Note that we can just mix the two classes together to combine 
 *   their behaviour.
 */


+ sallyImpatientAgenda: DelayedAgendaItem, ConvAgendaItem
    invokeItem()
    {
        if(me.hasSeen(oakDoor))
            "<.p><q>If you want answers you <i>have</i> to go through that oak
            door,</q> {the sally/she} tells you. ";
        else
            "<.p><q>You won't find anything down here,</q> {the sally/she} tells
            you, <q>we'll have to try upstairs.</q> ";
        
        isDone = true;
    }
;


//==============================================================================
/*   
 *   TOPIC
 *
 *   A Topic is used to represent a possible Topic of Conversation that's not
 *   otherwise represented as a physical object in the game world, either 
 *   because it's an abstraction (like 'weather' or 'troubles') or because 
 *   there's no physical object by that name corresponding to it in the game 
 *   (like the town).
 *
 *   Here we define just three Topics. A real game would probably define many
 *   more. Note that starting the name of a Topic object with a lower-case 
 *   t is not a requirement, it's simply a convention adopted here/
 *
 *   Note also that the only property we need to define on a Topic object is 
 *   its vocabWords (the single-quoted string in the Topic template). 
 */

tWeather: Topic 'weather';
tTown: Topic 'this town/place';
tTroubles: Topic 'troubles';