actor.t

documentation
#charset "us-ascii"
#include "advlite.h"


/*
 *   ****************************************************************************
 *    actor.t 
 *
 *    This module forms part of the adv3Lite library 
 *    (c) 2012-13 Eric Eve
 */

/* Declare the eventList property in case the eventList module isn't included */
property eventList;

/* Declare the inRoomName property in case the senseRegion module isn't included */
property inRoomName;

/*    
 *   An Actor is an object representing a living being (or something that
 *   behaves like a living being, such as an intelligent robot), with which the
 *   player character can potentially converse, and which can move around and
 *   pursue his/her/its own agenda. This class is intended for the
 *   implementation of NPCs (non-player characters).
 */
class Actor: EndConvBlocker, AgendaManager, ActorTopicDatabase, Thing
    
    /* 
     *   Our current ActorState. This should normally be treated as a read-only
     *   property; to change the current ActorState of an actor call the
     *   setState() method.
     */
    curState = nil
    
    /*   
     *   Set our current ActorState to a new state (stat) or to no state at all
     *   (if the stat parameter is supplied as nil).
     */
    setState(stat)
    {
        /* 
         *   Only do anything if the new state (stat) is different from our
         *   current state.
         */
        if(curState != stat)
        {
            /* 
             *   If the current state is non-nil, call its deactivateState()
             *   method to notify it that we're leaving it.
             */
            if(curState != nil)
               curState.deactivateState(self, stat);
            
            /*  Set out current state to the new state. */
            curState = stat;
            
            /*  
             *   If the new state is non-nil, call its activateState() method to
             *   notify it that we're entering it.
             */
            if(stat != nil)
               stat.activateState(self, curState);           
        }
    }
    
    
    /* 
     *   Our state-specific description, which is appended to our desc to give
     *   our full description. By default we simply take this from our current
     *   ActorState.
     */
    stateDesc = (curState != nil ? curState.stateDesc : '')
    
    /*   
     *   Our specialDesc (used to describe us in room listing). By default we
     *   use our ActorState's specialDesc if we have a current ActorState or
     *   else our actorSpecialDesc if our current ActorState is nil. But if
     *   there's a current FollowAgendaItem we let it handle the specialDesc
     *   instead.
     */
    specialDesc()
    {
        
        local followItem;
        
        /* 
         *   Check whether we have a FollowAgendaItem that's ready to be used,
         *   and if so, register it as our current followAgendaItem.
         */
        if(agendaList != nil &&
           (followItem = agendaList.valWhich({x: x.ofKind(FollowAgendaItem) &&
                                            x.isReady && !x.isDone})) != nil)
            followAgendaItem = followItem;
        
        /* If we have a current followAgendaItem, use its specialDesc */
        if(followAgendaItem != nil)
            followAgendaItem.showSpecialDesc();
        
        /* 
         *   Otherwise use our current ActorState's specialDesc if we have one
         *   or our our actorSpecialDesc if not.
         */
        else
            curState != nil ? curState.specialDesc : actorSpecialDesc;
    }
    
    
    /*   
     *   The specialDesc to use if we don't have a current ActorState By default
     *   we just display a message saying the actor is here or that the actor is
     *   in a nested room.
     */
    actorSpecialDesc()
    {	    
        /* 
         *   If this actor is the player character then we don't want to display
         *   anything by default here.
         */
        if(isPlayerChar)
            return;
        
        if(location == getOutermostRoom)
            DMsg(actor here, '\^<<theNameIs>> {here}. ');
        else
            DMsg(actor in location, '\^<<theNameIs>> <<location.objInName>>. ');        		
    }
    
    /*   
     *   We normally list any actors after the miscellaneous contents of a room
     */
    specialDescBeforeContents = nil
    
    /*   
     *   The specialDesc of this actor when it is viewed from a remote location.
     *   If we have a current ActorState we use its remoteSpecialDesc, otherwise
     *   we use the actorRemoteSpecialDesc on the actor. Either way the pov
     *   parameter is the point of view object from which this actor is being
     *   viewed (normally the player char).
     *
     *   Note that this method is generally only relevant if the senseRegion
     *   module is used.
     */
    remoteSpecialDesc(pov) 
    { 
        curState == nil ? actorRemoteSpecialDesc(pov) :
        curState.remoteSpecialDesc(pov);
    }
        
    /* 
     *   The remoteSpecialDesc to use if we don't have a current ActorState
     *   (i.e. if curState is nil). By default we say the actor is in the remote
     *   location.
     *
     */
    actorRemoteSpecialDesc(pov) 
    { 
        if(fDaemon == nil)
            DMsg(actor in remote location, '\^<<theNameIs>> 
                <<getOutermostRoom.inRoomName(pov)>>. ');
    }
       
    /*   
     *   By default actors can't be picked up and carried around by other actors
     *   (though game authors can override this if they need to create a
     *   portable actor).
     */
    isFixed = true    
    
    /*   The message to display when someone tries to take this actor. */
    cannotTakeMsg = BMsg(cannot take actor, '{The subj dobj} {won\'t} let {me}
        {dummy} pick {him dobj} up. ')
    
    /*   The (portable) contents of an actor are regarded as being carried. */
    contType = Carrier
    
    /* 
     *   We don't normally list the contents of an Actor when Looking or
     *   Examining.
     */
    contentsListed = nil
    
    /*   
     *   The default response of the actor to a conversational command that is
     *   not handled anywhere else.
     */
    noResponseMsg = BMsg(no response, '{The subj cobj} {doesnot respond[ed]}. ')
    
    
    /* Handle a command (e.g. BOB, JUMP) directed at this actor. */
    handleCommand(action)
    {
        /* 
         *   If the Command is GiveTo and the iobj is the player char, treat it
         *   as AskFor with the player char as the effective actor
         */        
        if(action.ofKind(GiveTo) && gCommand.iobj == gPlayerChar)
        {
            /* Change the current actor to the player char */
            gCommand.actor = gPlayerChar;
            
            /* Change the current action to AskFor */
            gAction = AskFor.createInstance();
            
            /* Install the appropriate objects in the new action. */
            gAction.curDobj == self;
            gAction.curIobj == gCommand.dobj;
            gAction.curObj = self;            
            
            /* 
             *   Handle the command as if the player had issued an AskFor
             *   command
             */
            handleTopic(&askForTopics, gCommand.dobj);
        }
        
        /* 
         *   if the command is TellAbout then convert the command from X,tell me
         *   about Y to ASK X ABOUT Y
         */
        else if(action.ofKind(TellAbout) && gCommand.dobj == gPlayerChar)
        {
            /* 
             *   if the command appears to ask the actor to tell the pc about
             *   the pc, the player probably intended to tell the actor to tell
             *   the pc about the actor (the reference of 'yourself' would be
             *   the actor, not the pc).
             */
            if(gCommand.iobj.topicList[1] == gPlayerChar)
                gCommand.iobj.topicList[1] = self;
            
            /* 
             *   since we've translated this into an ASK ABOUT command, the
             *   effective actor is now the player char.
             */
            gCommand.actor = gPlayerChar;
            
            /* Change the current action to AskFor */
            gAction = AskAbout.createInstance();
            
            /* Install the appropriate objects in the new action. */
            gAction.curDobj == self;
            gAction.curIobj == gCommand.dobj;
            gAction.curObj = self;
            
            
            /* Handle the command as AskFor */ 
            handleTopic(&askTopics, gCommand.iobj.topicList);
        }
        
        /* exclude SystemActions as a matter of course */
        else if(action.ofKind(SystemAction))
        {
            DMsg(cannot command system action, 'Only the player can carry out
                that kind of command. ');
        }
        
        /* treat Actor, hello as saying hello to the actor */
        else if(action.ofKind(Hello))
        {
            gCommand.actor = gPlayerChar;
            sayHello();
        }
        
        /* treat Actor, Bye as saying goodbye to the actor */
        else if(action.ofKind(Goodbye))
        {
            gCommand.actor = gPlayerChar;
            endConversation(endConvBye);
        }    
        
        /* treat Actor, question as directing a question to the actor */
        else if(action.ofKind(Query))
        {
            /* Make the player character the current actor */
            gCommand.actor = gPlayerChar;
            
            /* Note the query type on the current action */
            gAction.qType = gCommand.verbProd.qtype;
            
            /* Carry out the QueryTopic handling. */
            handleTopic(&queryTopics, gCommand.dobj.topicList);
        }
        
        /* treat Actor, (say) something as SAY SOMETHING TO ACTOR */
        else if(action.ofKind(SayAction))
        {
            /* Make the player character the current actor */
            gCommand.actor = gPlayerChar;            
            
            /* Carry out the SayTopic handling */
            handleTopic(&sayTopics, gCommand.dobj.topicList, &defaultSayResponse);
        }
        
        /* Otherwise try letting a CommandTopic handle it */
        else
        {
            /* 
             *   Note the direct and indirect objects of the command on the
             *   action.
             */
            action.curDobj = gCommand.dobj;
            action.curIobj = gCommand.iobj;
            
            /* Carry out the CommandTopic handling with the action commanded. */
            handleTopic(&commandTopics, action, &refuseCommandMsg);
        }
    }
    
    /*  
     *   The default message to use in response to a command directed to this
     *   actor that is not handled in any other way.
     */
    refuseCommandMsg = BMsg(refuse command, '{I} {have} better things to do. ')
        
    
    /* 
     *   Find the best response to the topic produced by the player's command.
     *   prop is the xxxTopics list property we'll use to search for a matching
     *   TopicEntry. We first search the current ActorState for a match and
     *   then, only if we fail to find one, we search TopicEntries directly
     *   located in the actor. First priority, however, is given to TopicEntries
     *   whose convKeys match this actor's currentKeys (they match if the two
     *   lists have at least one element in common).
     */    
    getBestMatch(prop, requestedList)    
    {
        /* 
         *   In the implementation of the conversation system we expect the prop
         *   parameter to be passed as a property pointer, but in the inherited
         *   handling it's a list. To avoid accidents, first check what we've
         *   got before converting the prop to a list.
         *
         */
        local myList;
        
        /* 
         *   If prop has been supplied as a property pointer, get the list
         *   defined on that property
         */
        if(dataType(prop) == TypeProp)
            myList = self.(prop);
        
        /*  
         *   Otherwise, if prop is simply a value, convert it directly to a list
         */
        else
            myList = valToList(prop);
        
        
        /* 
         *   If we have a current activeKeys list restrict the choice of topic
         *   entries to those whose convkeys overlap with it, at least at a
         *   first attempt. If that doesn't produce a match, try the normal
         *   handling.
         */             
        if(activeKeys.length > 0)
        {
            /* 
             *   Obtain a list of those items in myList (the list of topic
             *   entries we started with) that have convKeys that overlap with
             *   our activeKeys.
             */
            local kList = myList.subset({x:
                                   valToList(x.convKeys).overlapsWith(activeKeys)});
            
            /* 
             *   See if we can find a match by carrying out the inherited
             *   handling (from ActorTopicDatabase) with this sublist (for which
             *   the convKeys and activeKeys overlap)
             */
            local match = inherited(kList, requestedList);
            
            /*   If we find a match, simply return it and end there. */
            if(match != nil)
                return match;
        }
      
        /* 
         *   Otherwise carry out the inherited handling (from
         *   ActorTopicDatabase) with the complete list and return the result.
         */
        return inherited(myList, requestedList);
    }
    
    /*  
     *   Find the best response to use for a conversational command directed to
     *   this actor. prop would normally be a property pointer for the property
     *   containing the appropriate list or lists of Topic Entries to test, and
     *   topic is the Topic object we're trying to match.
     */
    findBestResponse(prop, topic)
    {
        local bestMatch;
        
        /* If we have a current ActorState, first try to get its best match */
        if(curState != nil)
        {
            /* Get the best matching TopicEntry from our current actor state. */
            bestMatch = curState.getBestMatch(prop, topic);
            
            /* If we found a result, return it and end there. */
            if(bestMatch != nil)
                return bestMatch;
        }
        
        /* 
         *   If we don't have a current ActorState, or we can't find a match on
         *   our current ActorState, find the best match from the TopicEntries
         *   located directly within the actor.
         */
        return getBestMatch(prop, topic);
    }
    
   
    /* 
     *   List of objects corresponding to non-conversation TopicEntry types for
     *   which an ActorState's noResponseMsg should not be used. We separate
     *   this lost out here to allow game code to add other game-specific types
     *   (e.g. hugTopicObj if the game implements a Hug Action and a HugTopic).
     */
    physicalTopicObjs = [hitTopicObj, kissTopicObj, touchTopicObj]
    
    /* 
     *   Handle a conversational command where prop is a pointer to the property
     *   containing the appropriate list of TopicEntries to search (e.g.
     *   &askTopics), topic is the list of Topics to match, and defaultProp is
     *   pointer to the property to invoke if we can't find a match.
     */
    handleTopic(prop, topic, defaultProp = &noResponseMsg)
    {
        /* 
         *   If the actor's current ActorState defines a noResponse property
         *   then we display it rather than trying to match any TopicEntries
         *   unless we're trying to match a non-conversational TopicEntry such
         *   as HitTopic, TouchTopic or KissTopic.
         */       
        if(curState && curState.propType(&noResponse) != TypeNil &&
           !topic.overlapsWith(physicalTopicObjs))
        {
            switch(curState.propType(&noResponse))
            {
            case TypeDString:
                curState.noResponse();
                return nil;
            case TypeSString:
                say(curState.noResponse);
                return nil;
            case TypeCode:
                if(gOutStream.watchForOutput({:curState.noResponse}))
                    return nil;               
            }
            
        }
        
        /* 
         *   Reset the keysManaged flag to nil so that we can end this method by
         *   carrying out the necessary keys management unless this is handled
         *   indirectly through the call to response.handleTopic() below, which
         *   may set this flag to true.
         */
        keysManaged = nil;
        
        /*  Note if we need a greeting response */
        local greetingResponse = nil;
        
        /*  Note our current actor state */
        local oldState = curState;
        
        /*   
         *   If we're not already in conversation with this actor, see if
         *   there's a HelloTopic or ImpHelloTopic that might change the
         *   ActorState
         */
        
        if(gPlayerChar.currentInterlocutor != self &&
           valToList(topic)[1] not in (helloTopicObj, impHelloTopicObj, 
                                       actorHelloTopicObj ))
        {
            /* Find a greeting response for an ImpHelloTopic/HelloTopic */
            greetingResponse = findBestResponse(&miscTopics, [impHelloTopicObj]);
            
            /* 
             *   If there is a greeting response and it wants to change state,
             *   temporarily change state before looking for the best match for
             *   the topic we're acually looking for
             */
            if(greetingResponse && greetingResponse.changeToState != nil)
                curState = greetingResponse.changeToState;
            
        }
        
	
        /* 
         *   Note the best response we can find; i.e. the TopicEntry from the
         *   prop list that best matches topic.
         */
        local response = findBestResponse(prop, topic);
        
        /*   
         *   Change back to our original state, since if the response we found
         *   isn't conversational or doesn't imply a greeting, we won't want to
         *   change state; if we have found a conversational response that
         *   implies a greeting, executing the greeting will change the state
         *   to the new state.
         */
        curState = oldState;
        
        /* 
         *   If we find a response, carry out an implied greeting if we need
         *   one, and then display the response.
         */
        if(response != nil)
        {    
            /* 
             *   Check whether we need to carry out an implied greeting. We need
             *   to do so if we're not already the player character's current
             *   interlocutor and the response we've found is a conversational
             *   one (i.e. one in which a conversational exchange actually takes
             *   place as opposed to one explaining why it can't or shouldn't).
             */
            if(gPlayerChar.currentInterlocutor != self &&
               response.isConversational)
            {            
                /* Make the player character the current interlocutor. */
                gPlayerChar.currentInterlocutor = self;
                
                /* 
                 *   Only try an implicit greeting if the response we've found
                 *   implies one (this prevents an implicit greeting from
                 *   forever trying to trigger itself, for example) and if we
                 *   found one to show.                 
                 */
                if(response.impliesGreeting && greetingResponse != nil)
                {
                    /* 
                     *   Carry out an implicit greeting. If this does anything
                     *   add a paragraph break to separate it from the
                     *   conversational exchange that follows.
                     */
                    greetingResponse.handleTopic();
                      "<.p>";
                }
            }
            
            /* 
             *   Let the response (the TopicEntry we've identified as the best
             *   match to the topic requested) handle the topic.
             */
            response.handleTopic();     
            
            /* 
             *   If the response is a converational one, note that conversation
             *   has taken place on this turn.
             */
            if(response.isConversational)
                noteConversed(); 
        }
        
        /* Otherwise, if we haven't found a matching response... */
        else
        {         
            /* 
             *   If we were speculatively trying an initiateTopic that doesn't
             *   actually find anything, don't count this as a conversational
             *   turn from the point of view of updating pending and active
             *   keys. The same applies if we were speculatively trying to find
             *   an ImpHelloTopic or any other topic with a nil defaultProp.
             */
            if(defaultProp == nil)
                return nil;
            
            /* Otherwise, show the default response */
            else                
            {               
                say(self.(defaultProp));    
            }
           
        }
        
        /* 
         *   Carry out the key management if it hasn't already been carried out
         *   on this turn. It may already have been by a tag handled by the
         *   conversationManager object processed via the call to
         *   response.handleTopic() above.
         */
        if(!keysManaged)
            manageKeys();
        
         /* 
          *   Return true or nil depending on whether we found a matching
          *   response to display
          */        
        return response != nil;
    }
    
    /* 
     *   Move pending keys to active keys and clear pending keys if need be. We
     *   call this out as a separate method to allow it to be directly called
     *   from elsewhere.
     */
    manageKeys()
    {
        /* 
         *   Reset the pending keys to nil unless we've been requested to retain
         *   them. (The pendingKeys are the set of convKeys that a previous
         *   conversational turn may have told us to match).
         */
        if(!keepPendingKeys)
            pendingKeys = [];
        
        /* Set our activeKeys to our pendingKeys */
        activeKeys = pendingKeys;
        
        /*  Reset the flag that tells us to keep our pending keys */
        keepPendingKeys = nil;  
        
        /* Note that we have now managed our keys. */ 
        keysManaged = true;		
    }
    
    /* 
     *   Flag; has the active/pending key management already been carried out on
     *   this turn?
     */
    keysManaged = nil
	
    /* Convenience method to note that conversation has occurred on this turn */    
    noteConversed()
    {
        /* Note that we're the player character's current interlocutor */
        gPlayerChar.currentInterlocutor = self;
        
        /* Note that we last conversed on this turn */
        lastConvTime = libGlobal.totalTurns;
        
        /* Note that this actor is a possible antecedent for a pronoun */
        notePronounAntecedent(self);
        
        /* Add our boredomAgendaItem to our agenda if it isn't already there */
        if(valToList(agendaList).indexOf(boredomAgendaItem) == nil)
            addToAgenda(boredomAgendaItem);
    }
    
    /* 
     *   This method can be called on the actor when we want to display the text
     *   of one or both sides of a conversational exchange with the actor
     *   without going through the TopicEntry mechanism to do so.
     */ 
    actorSay(str)
    {
        /* 
         *   Reset the keysManaged flag to nil so that we can end this method by
         *   carrying out the necessary keys management unless this is handled
         *   indirectly through the call to say(str) below, which may set this
         *   flag to true.
         */
        keysManaged = nil;
        
        /* Not that we have conversed with the actor this turn */		
        noteConversed();
        
        /* Display the text of the conversational exchange. */
        say(str);
        
        /* Carry out the keys management if it hasn't already been carried out. */
        if(!keysManaged)
            manageKeys();
    }
    
    
    /* 
     *   Conditionally use actorSay() or say() to output str depending on
     *   whether str appears to be something the actor says or simply a
     *   non-verbal response (or lack of response). If str contains quote marks
     *   or the sequence @@ we'll assume it's something the actor says (and
     *   strip out the @@ that would simply be there to mark str as something
     *   the actor says, perhaps reported in indirect speech.
     */     
    condSay(str)
    {
        /* 
         *   If str contains a quotation mark or @@ assume it's something this
         *   actor says.
         */
        if(str.find('<q>') || str.find('"') || str.find('@@'))
        {
            /* Strip out the @@ sequence, which is simply a marker. */
            str = str.findReplace('@@', '');
                
            /* 
             *   Display str using actorSay(), so that it counts as
             *   conversational.
             */
            actorSay(str);                
        }
        /* 
         *   Otherwise just use say(), since we don't want the response to count
         *   as conversational.
         */
        else
            say(str);   
    }
    
	
    /* 
     *   The last turn on which this actor conversed with the player character.
     *   We start out with a value of -1 to mean that we haven't conversed at
     *   all.
     */
    lastConvTime = -1
    
    /*  
     *   Has this actor conversed with the player character on the current turn?
     *   He/she/it has done so if our last conversation time is the same as the
     *   game's turn count.
     */
    conversedThisTurn = (lastConvTime == libGlobal.totalTurns)

    /*  
     *   Did this actor converse with the player character on the previous turn?
     *   He/she/it did so if our last conversation time is one less than the
     *   game's current turn count.
     */
    conversedLastTurn = (lastConvTime == libGlobal.totalTurns - 1)
    
    
    /* 
     *   If this list is not empty then the choice of topic entries to match
     *   will be restricted to those whose convKeys property includes at least
     *   one of the key values in this list.
     */
    activeKeys = []
    
    /* 
     *   a list of the keys to be copied into the activeKeys property for use in
     *   the next conversational turn. These are normally added by game code via
     *   <.convnode> tags and the like in conversational output.
     */
    pendingKeys = []
    
    /* 
     *   If keepPendingKeys is set to true (normally by a <.convstay> tag) then
     *   retain the pending conversation keys (and hence the active ones) for
     *   the next conversational turn.
     */    
    keepPendingKeys = nil
    
    /* 
     *   Add a convkey value to our pending keys list (for use as an active key
     *   on the next conversational turn.     */
    
    addPendingKey(val)
    {
        pendingKeys += val;
    }
    
    
    /*  Notification that an action is about to be carried out in our presence */
    beforeAction()
    {
        /* First execute our own actorBeforeAction() method */
        actorBeforeAction();
        
        /* 
         *   Then execute our current ActorState's beforeAction() method, if we
         *   have a current ActorState.
         */
        if(curState != nil)
            curState.beforeAction();
        
        
//        if(gActionIn (Take, TakeFrom) && gDobj.isIn(getActor))
//        {
//            /* Display a message saying that removing obj is disallowed. */
//            say(getActor.cannotTakeFromActorMsg(gDobj));
//            
//            /* Halt the action. */
//            exit;
//        }
        
    }
    
    /* 
     *   Give this actor a chance to respond just before an action prior to any
     *   response from its current actor state. By default we do nothing, but
     *   game code can easily override this without any risk of breaking the
     *   state-dependent beforeAction mechanism.
     */    
    actorBeforeAction()  { }
    
    /*  Notification that an action has just been carried out in our presence */
    afterAction()
    {
        /* First execute our own actorAfterAction() method */
        actorAfterAction();
        
        /* 
         *   Then execute the afterAction() method on our current ActorState, if
         *   we have one.
         */
        if(curState != nil)
            curState.afterAction();
           
    }
    
    /* 
     *   Give this actor a chance to respond just after an action prior to any
     *   response from its current actor state. By default we do nothing, but
     *   game code can easily override this without any risk of breaking the
     *   state-dependent afterAction mechanism.
     */ 
    actorAfterAction() { }
    
     /* 
      *   Notification that something else is about to travel. By default we
      *   defer to out actor state, if we have one, but we also give the actor
      *   object a chance to respond.
      */
    
    beforeTravel(traveler, connector) 
    {
        /* 
         *   If we have a current FollowAgendaItem, start by executing its
         *   beforeTravel() method.
         */
        
        if(followAgendaItem != nil && followAgendaItem.isReady)
            followAgendaItem.beforeTravel(traveler, connector);
        
        
        /* 
         *   Execute the beforeTravel() method on our current ActorState, if we
         *   have one.
         */
        if(curState != nil)
            curState.beforeTravel(traveler, connector);
        
        /*  Then execute our own actorBeforeTravel() method. */
        actorBeforeTravel(traveler, connector);
        
        /*  
         *   If the actor is waiting for the traveler to follow the actor via
         *   connector, then set the follow fuse instead of executing the
         *   actor's travel command.
         */        
        if(followAgendaItem != nil 
           && traveler == gPlayerChar 
           && followAgendaItem.isReady
           && followAgendaItem.nextConnector == connector)
        {

            setFollowMeFuse();
            exit;
        }
        
        
        /* 
         *   If the player char is talking to this actor and this actor is not
         *   following the player character, end the conversation.
         */
        
        if(gPlayerChar.currentInterlocutor == self && traveler == gPlayerChar
           && fDaemon == nil)
            endConversation(endConvLeave);
        
        /*  
         *   If the traveler that's about to travel is the player character,
         *   note the connector the player character is about to use.
         */
        if(traveler == gPlayerChar)
            pcConnector = connector;       
        
        
        
    }
    
    /* The Travel Connector just traversed by the player character */    
    pcConnector = nil
    
    /* 
     *   If the player character has seen this actor travel then lastTravelInfo
     *   contains a two-element list comprising the room the actor was seen
     *   travelling from and the connector by which the actor was seen
     *   travelling.
     *
     *   Note that if you move an actor by authorial fiat using moveInto() (say)
     *   when the player character can see the actor, you might want to update
     *   lastTravelInfo manually to ensure that any subsequent FOLLOW command
     *   still works properly, e.g.:
     *.
     *.   "Bob storms out through the front door, slamming it behind him. ";
     *.   bob.moveInto(nil);
     *.   bob.lastTravelInfo = [hall, frontDoor];
     *.
     *   (If instead of or before bob.moveInto(nil) you had written
     *   frontDoor.travelVia(bob), this wouldn't be necessary, since it would be
     *   handled for you by frontDoor.travelVia()).
     */
    lastTravelInfo = nil
    
    /* 
     *   Give this actor a chance to react just before another actor travels in
     *   addition to any reaction from its current actor state. By default we do
     *   nothing, but game code can easily override this without any risk of
     *   breaking the state-dependent beforeTravel mechanism.
     */ 
    actorBeforeTravel(traveler, connector) { }
       
    /* 
     *   Notification that travel has just taken place in our presence (usually
     *   because an actor has just arrived in our location)
     */    
    afterTravel(traveler, connector) 
    {
        /* If we have a current ActorState, execute its afterTravel() method */
        if(curState != nil)        
            curState.afterTravel(traveler, connector);
                   
        
        /* Execute our own actorAfterTravel() method */
        actorAfterTravel(traveler, connector);           
    }  
        
    /* The turn on which the player character last arrived in our location */
    pcArrivalTurn = nil
    
    /* Flag -- has the player character just arrived? */
    pcJustArrived = (pcArrivalTurn == gTurns)
        
    /* 
     *   Give this actor a chance to react just after another actor travels in
     *   addition to any reaction from its current actor state. By default we do
     *   nothing, but game code can easily override this without any risk of
     *   breaking the state-dependent afterTravel mechanism. 
     */     
    actorAfterTravel(traveler, connector) {}
      
    /*   
     *   Terminate a conversation that's currently going on between this actor
     *   and the player character. The reason parameter is the reason for ending
     *   the conversation and can be one of endConvBye (the player character has
     *   just said goodbye), endConvTravel (the player character is leaving the
     *   location), endConvBoredom (this actor has become bored with waiting for
     *   the player character to say anything) or endConvActor (this actor
     *   wishes to terminate the conversation for some other reason of its own).
     */
    endConversation(reason)
    {
        /* 
         *   If we're permitted to end the conversation for the reason
         *   specified, display a farewell message appopriate to the reason
         */
        if(canEndConversation(reason))
            sayGoodbye(reason);
        
        /* 
         *   otherwise if the player char is about to depart and the actor won't
         *   let the conversation end, block the travel
         */
        else if(reason == endConvLeave)
            exit;
    }
    
    /* 
     *   Is the actor willing for this conversation to be ended? We first check
     *   the current actor state (if any) and then the actor object. If either
     *   raises an object it should display a message saying what the objection
     *   is (and then return nil). By default we simply return true, allowing
     *   the conversation to end.
     */    
    canEndConversation(reason)
    {
        
        /* 
         *   First check whether there's a Conversation Node that wants to
         *   object to the conversation ending. We do that by first seeing if
         *   there's an active NodeEndCheck object...
         */
        local nodeCheck = findBestResponse(&initiateTopics, [nodeEndCheckObj]);
        
        /* 
         *... and if there is, seeing whether its canEndConversation() method
         *   objects.
         */        
        if(nodeCheck != nil && !nodeCheck.canEndConversation(reason))
            return nil;
        
        /* 
         *   Then check with the current ActorState (if there is one) and our
         *   own actorCanEndConversation() method.
         */
        if(curState == nil || curState.canEndConversation(reason))
            return actorCanEndConversation(reason);
        
        
        /* 
         *   If we've reached this point it's because our current ActorState has
         *   objected to ending the conversation, so return nil to disallow it.
         */
        return nil;
    }
    
    /* 
     *   A state-independent check on whether this actor will allow the current
     *   conversation to end on account of reason. By default we simply return
     *   true to allow the conversation to end, but game code can override this
     *   to return nil to disallow the ending of the conversation (presumably
     *   under specific conditions).
     */
    actorCanEndConversation(reason) { return true; }
    
    /*
     *   Do we want an otherwise not understood input (such as "You're crazy") treated as an
     *   implicit SAY command (e.g., treated as SAY YOU'RE CRAZY) when the player character is in
     *   conversatiom with this Actor? Return true if so or nil otherwise.
     */
    allowImplicitSay()
    {
        /* 
         *   If neither this Actor nor its current ActorState defines any SayTopics then there's no
         *   point treating any user input as an implicit SAY command, so simply return nil.
         */        
        if(autoImplicitSay && sayTopics.length == 0)
        {
            if(curState && curState.sayTopics.length > 0)
                return enableImplicitSay;
            else
                return nil;
        }
        
                     
        /*  
         *   If we don't want to count DefaultTopics as potential SayTopics for the purposes of
         *   interpreting otherwise not understood input, then check whether this Actor's sayTopic
         *   list includes anything that's not a DefaultTopic, and if not, return nil.
         */
        if(autoImplicitSay && !defaultCountsAsSay 
           && sayTopics.countWhich({x: !x.ofKind(DefaultTopic)}) == 0)
        {
            if(curState && curState.sayTopics.countWhich({x: !x.ofKind(DefaultTopic)}) > 0)
                return enableImplicitSay;
            else                
                return nil;
        }
                                   
        /*  Otherwise return the value of a user (game author) modifiable flag. */
        return enableImplicitSay;
    }
    
    
    /*
     *   User modifiable flag for use with the allowImplicitSay() method. Do we ever want to allow
     *   implicit SAY commands for this actor? By default we do allow this (true), since this has
     *   been the default library behaviour up until now, but game authors can change this either on
     *   individual Actors or by modifying the Actor class.
     */
    enableImplicitSay = true
    
    /*  
     *   Do we want a DefaultTopic to count as a SayTopic for the purpose of deciding whether to
     *   allow otherwise not understood commands being interpreted as implicit SAY commands? By
     *   default we don't, since that's most likely to be what game authors who don't explicitly
     *   define SayTopics (or DefaultSayTopics) intend, but game code can override this, either on
     *   individual actors or on the Actor class.
     */
    defaultCountsAsSay = nil
    
    
    /*
     *   Flag, do we want the allowImplicitSay() method to rule out the interpretation of commands
     *   as implicit Say commands if there are no available SayTopics for this actor? By default we
     *   do, but game code may wish to override this if the results of handling player input in this
     *   manner are felt to be inconsistent. If autoImplicitSay is set to nil then
     *   allowImplicitSay() will simply return the value of enableImplicitSay.
     */
    autoImplicitSay = true
    
    /* 
     *   Mechanism to allow this actor to follow the player char. We do this
     *   rather simplistically by checking whether the player char is still in
     *   our location and moving us to the player char's location if s/he is not
     *   on the assumption that if the player char can get there in one turn, so
     *   can we. On arriving in the player char's new location we announce that
     *   we've just followed the player char and then run the arrivingTurn
     *   method on our current actor state (if we have one).
     */    
    followDaemon()
    {
        /* First note which room we're currently in */
        local oldLoc = getOutermostRoom;
        
        /* 
         *   If we're not in the player character's current room, try to follow
         *   the player character
         */
        if(getOutermostRoom != gPlayerChar.getOutermostRoom)
        {
            /* 
             *   If we know which TravelConnector the player character left by,
             *   try to traverse it.
             */
            if(pcConnector != nil)                
                pcConnector.travelVia(self);
            
            /* 
             *   Otherwise, simply travel to the player character's current room
             */
            else
                gPlayerChar.getOutermostRoom.travelVia(self);
            
            /*  
             *   Display our message to say we're following the player character
             */
            sayFollowing(oldLoc, pcConnector);
            
            /* 
             *   Carry out any additional handling we want to do on arriving in
             *   our new location.
             */
            arrivingTurn();            
        }        
        
        /* 
         *   Reset pcConnector to nil in any event so that a spurious value
         *   isn't left for a later turn.
         */
        pcConnector = nil;
    }
    
    /* 
     *   Game code can call this method to instruct this actor to start
     *   following the player char round the map
     */    
    startFollowing()
    {
        /* 
         *   Create a new Daemon to carry out the following and make a note of
         *   it
         */
        if(fDaemon == nil)
            fDaemon = new Daemon(self, &followDaemon, 1);
    }
    
    /* 
     *   Game code can call this method to instruct this actor to stop following
     *   the player char round the map.
     */    
    stopFollowing()
    {
        /* 
         *   If there's a currently active following Daemon, remove it from the
         *   game's list of events to be executed each turn.
         */
        if(fDaemon != nil)
            fDaemon.removeEvent();
        
        /*   Note that we no longer have an active following Daemon */
        fDaemon = nil;
        
        /*   
         *   Reset pcConnector to nil so that we don't leave an old spurious
         *   value for a later turn.
         */
        pcConnector = nil;
    }
    
    /* 
     *   Store the id of the daemon being used to make us follow the player
     *   char. We can check whether this actor is currently following or not by
     *   testing whether or not this is nil.
     */    
    fDaemon = nil
    
    /*   
     *   Display a message to say that we've just followed the player character
     *   to a new location from oldLoc.
     */
    sayFollowing(oldLoc, conn)
    {
        /* 
         *   If we don't have a current ActorState, use our own
         *   sayActorFollowing() method to say we've just followed the player
         *   character.
         */
        if(curState == nil)
            sayActorFollowing(oldLoc, conn);
        
        /*  Othewise call the sayFollowing() method on our current ActorState */
        else
            curState.sayFollowing(oldLoc, conn);
    }
    
    /*  
     *   Display a message to say that we've just followed the player character
     *   to a new location from oldLoc. The library provides a default message
     *   but this can be overridded as desired.
     */
    sayActorFollowing(oldLoc, conn)
    {
        /* 
         *   Create some local variables to use as message substitution
         *   parameters.
         */
        local follower = self, pc = gPlayerChar;
        gMessageParams(follower, pc);
        
        /* Display our default following message */
        DMsg(follow, '<.p>{The follower} follow{s/ed} behind {the pc}. ');
    }
    
    
    /* 
     *   In addition to providing a mechanism to allow an actor to follow the
     *   player character around (above) we provide a few methods to enable the
     *   player character to follow an actor (below). We do this by having the
     *   the FOLLOW command set a fuse which, when it is triggered later on the
     *   same turn, attempts to make the player character follow the target
     *   actor either if the target actor has just moved away from the player
     *   character's current location later on the same turn as the FOLLOW
     *   command was issued or if the player character is in the location from
     *   which he last saw the target actor depart, in which case the player
     *   character attempts to traverse the TravelConnector through which s/he
     *   saw the actor depart.
     */
    
    /*   
     *   Set the fuse to enable travel later on the same turn if this actor
     *   travels in the meantime. This method is called when a FOLLOW command is
     *   issed with this actor as its direct object.
     */
    setFollowMeFuse()
    {
        /* reset the travel info */
        lastTravelInfo = nil;
        
        /* set up a new fuse */ 
        followFuseID = new Fuse(self, &followFuse, 0);
        
        /* give it a low priority so any events that move the actor fire first */
        followFuseID.eventOrder = 100000;
        
        /* 
         *   Suppress the next paragraph break (otherwise we get an unnecessary
         *   blank line after a FOLLOW command)
         */
        "<.p0>";
    }
    
    /* 
     *   A note of our current following fuse, if we have one; this is used by
     *   FollowAgendaItem to check whether the player character is ready to
     *   follow us.
     */
    followFuseID = nil
    
    /*   
     *   This method is executed right at the end of a turn on which the player
     *   has issued a command to follow this actor, and carries out the travel
     *   to follow this actor if the actor has traveled.
     */
    followFuse()
    {
        /* 
         *   If we have information relating to this actor's last travel
         *   movements, then follow this actor.
         */
        if(lastTravelInfo)
        {
                        
            /* Display a message saying that we're following this actor. */
            sayActorFollowingMe(lastTravelInfo[2]);
            
            /* 
             *   Make the following actor travel via the TravelConnector last
             *   traversed by this actor.
             */
            lastTravelInfo[2].travelVia(gActor);
        }
        
        /* 
         *   Otherwise display a message saying that the actor hasn't moved, but
         *   only if the followAgendaItem hasn't already reported it.
         */
        else
        {
            if(followAgendaItem == nil 
               || followAgendaItem.travelBlockReported
               == nil)
            say(actorStaysPutMsg);
        }
        
        /* Reset the following fuse ID to nil */
        followFuseID = nil;
        
        /* 
         *   If we have a current FollowAgendaItem and it's finished with, note
         *   that we no longer have a current FollowAgendaItem.
         */
        if(followAgendaItem != nil && followAgendaItem.isDone)
        {     
            /* Finally note that we've arrived at our destination */
            followAgendaItem.noteArrival();
            
            followAgendaItem = nil;
        }
    }

    /* The message to display when another actor follows this one. */
    sayActorFollowingMe(conn)
    {       
        /* 
         *   If we have a current followAgendaItem, let it handle it in the
         *   first instance.
         */
        if(followAgendaItem != nil)
            followAgendaItem.sayDeparting(conn);
        
        /* Otherwise, let the connector handle it. */
        else                
            conn.sayActorFollowing(gActor, self);
    }
    
    followActorMsg = BMsg(follow actor, '{I} follow{s/ed} {1}. ', theName)
    
    /* 
     *   The message to display when this actor doesn't go anywhere when the
     *   player character tries to follow this actor.
     */
    actorStaysPutMsg = BMsg(actor stays put, '{I} wait{s/ed} in vain for {1} to
        go anywhere. ', theName)
    
    
    /* Our currently executing FollowAgendaItem, if we have one. */
    followAgendaItem = nil
    
    /* 
     *   Display a message describing this actor's departure via conn. This
     *   looks a bit circuitous in that this method calls the corresponding
     *   method on the current ActorState, which by default calls our own
     *   sayActorDeparting() method, which in turn calls sayDeparting on the
     *   connector; the idea is to allow customization at any point with the
     *   connector's sayDeparting() method simply providing a fallback to a
     *   colourless default. Note, however, that game code shouldn't normally
     *   override the actor's sayDeparting() method, but should instead
     *   intervene either on the ActorState or on the actor's
     *   sayActorDeparting() method. Note also that the purpose of this method
     *   is to describe an NPC's departure from the point of view of the player
     *   character, not to describe the player character's movements.
     */
    sayDeparting(conn)
    {
        /* If we have a current ActorState, call its sayDeparting() method */
        if(curState != nil)
            curState.sayDeparting(conn);
        
        /* Otherwise, call our own sayActorDeparting() method */
        else
            sayActorDeparting(conn);
    }
    
    /*  
     *   Method to display a message saying that this actor (normally an NPC
     *   visible to the player character) is departing via conn (a
     *   TravelConnector object, which may be a Room as well as a Door or other
     *   kind of connector). Note that the default behaviour of
     *   ActorState.sayDeparting is simply to call this method.
     */
    sayActorDeparting(conn)
    {
        /* 
         *   By default we let the connector describe the departure in a manner
         *   appropriate to the kind of connector it is.
         */
        conn.sayDeparting(self);
    }
    
    /* 
     *   This method is executed when this actor has just followed the player
     *   character to a new location.
     */
    arrivingTurn()
    {
        /* If we have a current ActorState, execute its arrivingTurn() method */
        if(curState != nil)
            curState.arrivingTurn();
        
        /* Otherwise execute our own actorArrivingTurn() method. */
        else
            actorArrivingTurn();
    }
    
    /* 
     *   This method is executed when this actor has just followed the player
     *   character to a new location and there is no current ActorState. By
     *   default we do nothing.
     */
    actorArrivingTurn() { }
        
    /* 
     *   The message to display when the player char sees this actor arriving
     *   after traveling from loc.
     */
    sayArriving(fromLoc)
    {
       /* If we have a current ActorState, call its sayArriving() method */
        if(curState != nil)
            curState.sayArriving(fromLoc);
        
        /* Otherwise, call our own sayActorArriving() method */
        else
            sayActorArriving(fromLoc);   
    }
    
    /* 
     *   Default message to display when the player character sees this actor
     *   arriving. We use a very plain-vanilla message here, since in practice
     *   game code will generally want to override this.
     */
    sayActorArriving(fromLoc)
    {
        local traveler = self;
        gMessageParams(traveler);
        
        DMsg(actor arriving, '{The subj traveler} arrive{s/d} in the area. ');
    }
    
    
    /* 
     *   Make this actor travel via the connector conn and report its departure.
     *   If announceArrival is true (the default) we also announce the actor's
     *   arrival (if it's visible to the player char).
     *
     *   To suppress the default arrival announcement altogether, supply the
     *   second optional parameter as nil. In some cases it may be easier to do
     *   this and supply your own custom arrival message after calling
     *   travelVia() than to juggle with the various sayArriving() methods.
     */     
    travelVia(conn, announceArrival = true)
    {
        local wasSeenLeaving = nil;
        local oldLoc = location;
        
        if(Q.canSee(gPlayerChar, self))
        {           
            /* Note that we were seen leaving. */
            wasSeenLeaving = true;
        }

        /* 
         *   If the player character can see this actor, display a message
         *   indicating this player's departure.
         */        
        if(wasSeenLeaving)
            sayDeparting(conn);
        
        /* Move this actor via conn. */
        conn.travelVia(self);
        
        if(announceArrival && !wasSeenLeaving && Q.canSee(gPlayerChar, self))
            sayArriving(oldLoc);
    }
       
    /*  
     *   The takeTurn() method is called on every Actor every turn to carry out
     *   a number of housekeeping functions relating to the conversation and
     *   agenda item systems.
     */
    takeTurn()
    {      
        
        /* 
         *   First, if we're the current interlocutor, check that we can
         *   still talk to the player character. If not, make us no longer the
         *   current interlocutor so we don't respond to conversational commands
         *   when we're no longer there.
         */        
        if(gPlayerChar.currentInterlocutor == self && 
           !canTalkTo(gPlayerChar))
        {
            /* Reset the player character's current interlocutor to nil */
            gPlayerChar.currentInterlocutor = nil;
            
            /* 
             *   Reset our active and pending conversation keys so we don't
             *   behave as if we were still in an active conversation node.
             */
            activeKeys = [];
            pendingKeys = [];
            
            /* Terminate the method there; we've done enough for this turn. */
            return;
        }
        
        /*  
         *   Next, if we haven't already conversed this turn, and we have active
         *   conversation keys (meaning that we might be in a Conversation
         *   Node), try executing the NodeContinuationTopic associated with our
         *   current node (this can be used to nudge the player's memory that
         *   we're expecting an answer to a question we've just asked). If we
         *   find one and execute it, end there.
         */
        if(!conversedThisTurn && activeKeys.length > 0 &&
           initiateTopic(nodeObj))
                        return;
        
        
        /* 
         *   Next, if we haven't conversed this turn, try executing our highest
         *   priority AgendaItem, if we have one.
         */
        if(!conversedThisTurn && !executeAgenda)
        {
            /* 
             *   If we haven't conversed this turn and we didn't find an
             *   AgendaItem to execute, then, if we have a current ActorState
             *   that's been mixed in with a Script class (typically some kind
             *   of EventList), execute our current ActorState's curScript
             *   method, provided the player character can see us. This allows
             *   an ActorState to display a series of 'fidget messages' or the
             *   like for an actor who hasn't otherwise done anything this turn.
             */
            if(curState != nil && curState.ofKind(Script) 
               && Q.canSee(gPlayerChar, self))
                curState.doScript();
        }
        
        /* 
         *   If we haven't conversed this term and we're meant to be in
         *   conversation with the player character, increment our boredomCount
         *   by one; this may eventually lead to this actor terminating the
         *   conversation of its own accord.
         */
        if(!conversedThisTurn && gActor.currentInterlocutor == self)        
            boredomCount++;
            
        /*  Otherwise reset the boredomCount to zero */
        else
            boredomCount = 0;           
    }
    
    
    
    /* 
     *   our special "boredom" agenda item - this makes us initiate an end
     *   to an active conversation when the PC has ignored us for a given
     *   number of consecutive turns 
     */
    boredomAgendaItem = perInstance(new BoredomAgendaItem(self))

    
    /* 
     *   If this is non-nil then a TOPICS command will use it to restrict the
     *   topics suggested to those with this key in their convKeys property.
     *   This could be used, for example, to provide a top-level 'menu' of
     *   topics when the full list would otherwise be overwhelming.
     */
    suggestionKey = nil

    /* 
     *   Show a list of topics the player character might want to discuss with
     *   this actor. The explicit flag is true if the player has explicitly
     *   requested the topic list via a TOPICS command. The tag parameter can be
     *   a single convKey tag or a list of convKey tags; if tag is nil or 'all'
     *   then we don't restrict the suggestions by tag, otherwise we restrict
     *   the suggestions to those that match the tag (or one of the tags in the
     *   list of tags).
     */
    showSuggestions(explicit = true, tag = (pendingKeys == [] ? suggestionKey
                                            : pendingKeys))
    {
        /* 
         *   Start by creating a list of listable topics (i.e. those topics that
         *   could be reached by a conversational command issued by the player
         *   on the next turn)
         */
        local lst = listableTopics;
        
        /* 
         *   If we have a current ActorState, add its listableTopics to our
         *   list.
         */
        if(curState != nil)
            lst += curState.listableTopics;
        
        /* 
         *   If the tag parameter has been passed as a list, then for each tag
         *   in the list find all the matching TopicEntries, then find the
         *   intersect of all the listable TopicEntries with those that match
         *   all the tags.
         */
        if(dataType(tag) == TypeList)
        {
            /* Create a new empty Vector */
            local vec = new Vector(10);
            
            /* 
             *   Go through each tag in our list, looking it up in our
             *   convKeyTab table and adding all the corresponding TopicEntries
             *   (that match the tag) to our Vector.
             */
            foreach(local t in tag)
            {
                vec.appendUnique(valToList(convKeyTab[t]));
            }
            
            /* 
             *   Restrict our list of TopicEntries to those that are also found
             *   in the Vector of TopicEntries that match one of our tags.
             */
            lst = lst.intersect(vec.toList());    
        }
        
        /* 
         *   Otherwise, if the tag parameter is supplied, use it to provide a
         *   sublist of only those topics with a convKeys property matching the
         *   tag. A tag of 'all' is treated as a special value to allow a
         *   <.suggest all> tag to list all available special topics.
         */
        else if(tag not in (nil, 'all'))
            lst = lst.intersect(valToList(convKeyTab[tag]));
        
        /* 
         *   Use the suggestedTopicLister to show a list of Suggested Topics
         *   from the resulting list (lst).
         */
        suggestedTopicLister.show(lst, explicit);            
        
    }
    
        
     /* 
      *   A Lookup Table holding conversation keys. Entries in this list take
      *   the form tag -> list of TopicEntries that match this tag (e.g. the key
      *   is a convKey tag, expressed as a single-quoted string, and the value
      *   is a list containing TopicEntries whose convKeys property contains
      *   that tag).
      */      
    convKeyTab = nil
    
    /* 
     *   Set the curiosityAroused flag to true for all topic entries with this
     *   convKey. This allows topics to be suggested when and only when the
     *   player character has some reason to be curious about them, even though
     *   they were actually available before.
     */
    arouse(key)
    {
        /* 
         *   First check that we actually have any entries in our convKeyTab
         *   before we attempt to use them.
         */
        if(convKeyTab != nil)
            /* 
             *   If we do then go through every TopicEntry that has key amongst
             *   its convKeys (which we can obtain by looking up the list of suh
             *   TopicEntries in our convKeysTab) and set its curiosityAroused
             *   property to true.
             */
            foreach(local cur in valToList(convKeyTab[key]))
        {
            cur.curiosityAroused = true;
        }
    }
    
    /* 
     *   Set the activated flag to true for all topic entries with this convKey.
     *  
     */    
    makeActivated(key)    
    {
        /* 
         *   First check that we actually have any entries in our convKeyTab
         *   before we attempt to use them.
         */
        if(convKeyTab != nil)
        {
            /* 
             *   If we do then go through every TopicEntry that has key amongst
             *   its convKeys (which we can obtain by looking up the list of suh
             *   TopicEntries in our convKeysTab) and set its activated property
             *   to true.
             */
            foreach(local cur in valToList(convKeyTab[key]))
                cur.activate();
        }
    }
    
    
    /* 
     *   Set the activated flag to nil for all topic entries with this convKey.
     *  
     */    
    makeDeactivated(key)    
    {
        /* 
         *   First check that we actually have any entries in our convKeyTab
         *   before we attempt to use them.
         */
        if(convKeyTab != nil)
        {
            /* 
             *   If we do then go through every TopicEntry that has key amongst
             *   its convKeys (which we can obtain by looking up the list of suh
             *   TopicEntries in our convKeysTab) and set its activated property
             *   to nil.
             */
            foreach(local cur in valToList(convKeyTab[key]))
                cur.deactivate();
        }
    }
    
    /* 
     *   A list of all ConvNodes associated with this actor. This is a list of
     *   two element lists of the form [node-name, node-obj] where node-name is
     *   the convKeys property of the node and node is the associated object.
     */
    allNodes = []
        
    
    /* 
     *   Service method used by curNodeKey() and curNodeObj() to identify the
     *   current ConvNode object, if there is one.
     */
    curNodeIdx()
    {
        /* 
         *   If we have a current ConvNode, it will be the one specified by the
         *   first element in our activeKeys list.
         */
        local node = activeKeys.element(1);
        
        /*   If we don't have any activeKeys, then we don't have a ConvNode */
        if(node == nil)
            return nil;
        
        /*  
         *   The first activeKey may not be a ConvNode, so look it up in our
         *   list of ConvNodes (allNodes) to see if it's there and return its
         *   place in the list.
         */
        return allNodes.indexWhich({n: n[1] == node});
    }
    
    /*   The string name of our current convNode, if we have one. */
    curNodeKey()
    {
        /* 
         *   Get the index within allNodes of our current ConvNode, if there is
         *   one.
         */
        local idx = curNodeIdx;
        
        /*   
         *   If we found an index, return the key (first) element of the
         *   corresponding item in our allNodes list. Otherwise return nil.
         */
        return idx == nil ? nil : allNodes[idx][1];
    }
    
    /*   The object representing our current convNode, if we have one. */
    curNodeObj()
    {
        /* 
         *   Get the index within allNodes of our current ConvNode, if there is
         *   one.
         */
        local idx = curNodeIdx;
        
        /*   
         *   If we found an index, return the object (second) element of the
         *   corresponding item in our allNodes list. Otherwise return nil.
         */
        return idx == nil ? nil : allNodes[idx][2];
    }
    
    
    
    /* 
     *   We supply a getActor method that returns self so that objects such as
     *   TopicEntries that may be located either directly or indirectly in us
     *   can get at their associated actor by simply calling getActor on their
     *   immediate location; at some point such a chain of calls to
     *   location.getActor will end here.
     */    
    getActor { return self; }
    
    /*   
     *   The count of how many turns have passed during which no conversation
     *   has actually taken place when we're the player charater's current
     *   interlocutor. This can be used to terminate the conversation through
     *   'boredom' if the boredomCount exceeds our attention span.
     */
    boredomCount = 0
    
    
    /*  
     *   The maximum value that our boredomCount can reach before we terminate a
     *   conversation through 'boredom', because we've given up waiting for the
     *   player character to say anything. A value of nil (the default) meanns
     *   that we never terminate a conversation for this reason.
     */
    attentionSpan = nil
    
    /* Our look up table for things we've been informed about */    
    informedNameTab = nil
    
    
    /* 
     *   Note that we've been informed of something, by adding it to our
     *   informedNameTab. Tag is an arbitrary single-quoted string value used to
     *   represent the information in question.
     */    
    setInformed(tag)
    {
        if(informedNameTab == nil)
            informedNameTab = new LookupTable(32, 32);
        
        informedNameTab[tag] = true;
    }
    
    /* 
     *   Determine whether this actor has been informed about tag. We return
     *   true if there is a corresponding non-nil entry in our informedNameTab
     */
    informedAbout(tag) 
    {        
        return informedNameTab == nil ? nil : informedNameTab[tag] != nil;     
    }
    
    /*  
     *   Should other actors who can notionally hear the PC talking to us
     *   overhear when information is imparted to us? I.e. should their
     *   setInform() methods be called too? If we have a curState we use its
     *   setting, otherwise we use the value of actorInformOverheard.
     */
    informOverheard = (curState == nil ? actorInformOverheard :
    curState.informOverheard)
    
    /*  
     *   Should other actors who can notionally hear the PC talking to us
     *   overhear when information is imparted to us when our current ActorState
     *   is nil? By default they should.
     */
    actorInformOverheard =  true


    /* 
     *   Say hello to the actor (when the greetin is initiated by the player
     *   character)
     */
    sayHello()
    {
        /* 
         *   Only carry out the  full greeting if we're not already the player
         *   character's current interlocutor.
         */
        if(gPlayerChar.currentInterlocutor != self)
        {
            /* 
             *   Note that we are now the player character's current
             *   interlocutor
             */
            gPlayerChar.currentInterlocutor = self;
            
            /*  Look for an appropriate HelloTopic to handle the greeting. */
            handleTopic(&miscTopics, [helloTopicObj], &noHelloResponseMsg);
        }
        /* 
         *   Otherwise display a message to say that we're already talking to
         *   this actor.
         */
        else            
            condSay(alreadyTalkingMsg);
        
        /* Add a paragraph break */
        "<.p>";
        
        /* Display a list of not-explicitly-asked-for topic suggestions */
        showSuggestions(nil, suggestionKey);
    }
    
    /* Have the actor greet the player character on the actor's initiative */
    actorSayHello()    
    {
        /* 
         *   First check that we're not already the player character's current
         *   interlocutor before issuing a greeting.
         */
        if(gPlayerChar.currentInterlocutor != self)
        {
            /* 
             *   Note that we have conversed with the player character on this
             *   turn.
             */
            noteConversed();
            
            /*  
             *   Find an appropriate ActorHelloTopic to handle the greeting; if
             *   we don't find one, use our nil Response (i.e., don't display
             *   anything)
             */
            return handleTopic(&miscTopics, [actorHelloTopicObj], nil);
        }       
        
        /* Return nil to signal we didn't actually do anything */
        return nil;
    }
    
    /* 
     *   Say goodbye to this actor (farewell from the player character). The
     *   optional reason parameter is the reason we're saying goodbye, which
     *   defaults to endConvBye (i.e. the player character saying goodbye)
     */
    sayGoodbye(reason = endConvBye)
    {
        /* 
         *   If we've not the player character's current interlocutor and the
         *   player character tries to say goodbye to us, display a message
         *   saying that the player character isn't talking to us.
         */
        if(gPlayerChar.currentInterlocutor != self && reason == endConvBye)
        {
            DMsg(not interlocutor, '{I}{\'m} not talking to {1}. ', theName);
        }        
        else
        {
            /* 
             *   Otherwise find the appropriate kind of ByeTopic to handle the
             *   farewell, which will vary according to the reason for the
             *   farewell.
             */
            handleTopic(&miscTopics, [reason], 
                        reason == endConvBye ? &noGoodbyeResponseMsg : nil);
            
            /* 
             *   Then note that we are no longer in conversation with the player
             *   character.
             */
            gPlayerChar.currentInterlocutor = nil;
        }
    }
    
    /* 
     *   The message to display when someone says hello to this actor but
     *   there's no accessible HelloTopic defined.
     */
   
    noHelloResponseMsg = BMsg(no hello response, '{I} now {have} {1} attention.
        ', possAdj)
    
    alreadyTalkingMsg = BMsg(already talking, '{I} {am} already talking to {1}.
        ', theName)
    
    noGoodbyeResponseMsg = BMsg(no goodbye response, 'The conversation{dummy}
        {is} ended. ')
    
    
    /* Do nothing if we can't fine a suitable Hello or Bye Topic/ */    
    nilResponse() { }
    
    /* 
     *   A list of all the ActorStates associated with this Actor; this is
     *   populated by the preinitialization of the individual ActorStates.
     */
    allStates = []
    
    /* 
     *   Is this actor ready to invoke a ConvAgendaItem? We're ready if we
     *   haven't conversed this term and we can speak to the other actor and
     *   we're not at a conversation node. This method is used by the isReady
     *   property of ConvAgendaItem (to save it having to make three separate
     *   calls to getActor).
     */
    
    convAgendaReady(other)
    {
        return !conversedThisTurn && canTalkTo(other) && activeKeys.length == 0;
    }
    
    
    /* Convenience methods for adding agenda items */
    
    /* 
     *   Add an agenda item to both myself and any DefaultAgendaTopic directly
     *   within me.
     */    
    addToBothAgendas([lst])
    {
        addToAgenda(lst...);
        if(defaultAgendaTopic != nil)
            defaultAgendaTopic.addToAgenda(lst...);
    }
    
    /* 
     *   Add an agenda item both to myself and to any DefaultAgendaTopics either
     *   directly in me or in any of my Actor States
     */    
    addToAllAgendas([lst])
    {
        addToBothAgendas(lst...);
        foreach(local state in allStates)
        {
            if(state.defaultAgendaTopic != nil)
                state.defaultAgendaTopic.addToAgenda(lst...);
        }      
    }
    
    /* 
     *   Add an agenda item to myself and to any DefaultAgendaTopios directly in
     *   me or in my current ActorState.
     */    
    addToCurAgendas([lst])
    {
        addToBothAgendas(lst...);
        if(curState != nil && curState.defaultAgendaTopic != nil)
            curState.defaultAgendaTopic.addToAgenda(lst...);
    }
    
    /*  
     *   Remove an agenda Item both from this actor and from any associated
     *   DefaultAgendaTopics directly within this actor.
     */
    removeFromBothAgendas([lst])
    {
        removeFromAgenda(lst...);
        if(defaultAgendaTopic != nil)
            defaultAgendaTopic.removeFromAgenda(lst...);
    }
    
    /*  
     *   Remove an agenda Item both from this actor and from any associated
     *   DefaultAgendaTopics directly within this actor or in any of its
     *   ActorStates.
     */
    removeFromAllAgendas([lst])
    {
        removeFromBothAgendas(lst...);
        foreach(local state in allStates)
        {
            if(state.defaultAgendaTopic != nil)
                state.defaultAgendaTopic.removeFromAgenda(lst...);
        }      
    }
    
    /* 
     *   Remove an agenda item from myself and from any DefaultAgendaTopios
     *   directly in me or in my current ActorState.
     */
    removeFromCurAgendas([lst])
    {
        removeFromBothAgendas(lst...);
        if(curState != nil && curState.defaultAgendaTopic != nil)
            curState.defaultAgendaTopic.removeFromAgenda(lst...);
    }
    
    /* 
     *   A list of agenda items to be added to our agenda at some later point.
     *   The main purpose is to allow game code to set up a list of AgendaItems
     *   (typically ConvAgendaItems) that become part of the actor's current
     *   agenda when conversation is initiated via a HelloTopic.
     */    
    pendingAgendaList = []
    
    /* Add an item to our pending agenda list */    
    addToPendingAgenda([lst])
    {
        foreach(local item in lst)
            pendingAgendaList += item;
    }
    
    /* 
     *   Make our pending agenda items acting by moving them all from our
     *   pendingAgendaList to all our actual agenda lists (on the actor and on
     *   all our DefaultAgendaItems).
     */
    activatePendingAgenda()
    {
        foreach(local cur in pendingAgendaList)
            addToAllAgendas(cur);
        
        pendingAgendaList = [];
    }
    
    /*  Remove one or more agenda items from our pending agenda */
    removeFromPendingAgenda([lst])
    {
        foreach(local item in lst)
            pendingAgendaList -= item;
    }
      
    
    /*  
     *   Respond to an InitiateTopic triggered on this actor with top as the
     *   matching object
     */
    initiateTopic(top)
    {        
        /* 
         *   Try our current actor state first, if we have one, and only if it
         *   fails to find a response try handling the initiateTopic on the
         *   actor.
         */
        if(curState != nil && curState.initiateTopic(top))
            return true;
        
        return inherited(top);
    }
    
    /* 
     *   The notifyRemove() method is triggered when actionMoveInto() tries to
     *   move an object that's located within this actor. By default we don't
     *   allow it since it typically represents an attempt by the player
     *   character to take something from this actor's inventory.
     */
    notifyRemove(obj)
    {
        /* 
         *   If we're not the actor initiating the moving of obj and we don't
         *   allow this object to be removed from us, prevent the move.
         */
        
        if(gActor != self && !allowOtherActorToTake(obj))
        {         
           
            /* Display a message saying that removing obj is disallowed. */
            say(cannotTakeFromActorMsg(obj));
                        
            /* Halt the action. */
            exit;
        }    
    }
    
    /* 
     *   Return a message saying that the actor cannot take obj from our
     *   inventory.
     */
    cannotTakeFromActorMsg(obj)
    {
        /* 
         *   Set up a convenient pair of message substitution parameters to use
         *   in the mesage.
         */
        local this = self;
        gMessageParams(obj, this);
               
        /* Return the text of the message. */
        return BMsg(cannot take from actor, '{The subj this} {won\'t} let {me}
            have {the obj} while {he obj}{\'s} in {her this} possession. ');
    }
    
    /* 
     *   Is another actor allowed to take obj from our inventory? By default we
     *   return nil to disallow it for all objects.
     */
    allowOtherActorToTake(obj) { return nil; }
    
    /* An actor generally owns its contents */
    ownsContents = true
    
    /* 
     *   This definition is needed for the TopicGroup implementation, and should
     *   not normally be overridden in user game code. It allows TopicEntries
     *   and TopicGroups to determine their own active status by reference to
     *   that of their immediate location.
     */
    active = true
    
    /*   
     *   This property can be used by extensions or game code that wants to
     *   notify actors in some order of priority.
     */
    notificationOrder = 100
    
    /*
     *   ***********************************************************************
     *   ACTION HANDLING
     *****************************************************************/
         
    /* In general we can talk to actors */
    canTalkToMe = true
    
    dobjFor(TalkTo)
    {  
        action()
        {
            sayHello();
        }
    }
    
    dobjFor(AskAbout)
    {     
        action()
        {
            handleTopic(&askTopics, gIobj.topicList);
        }
    }
    
    dobjFor(AskFor)
    {     
        action()
        {
            handleTopic(&askForTopics, gIobj.topicList);
        }
    }
    
    dobjFor(TellAbout)
    {
        action()
        {
            handleTopic(&tellTopics, gIobj.topicList);
        }
    }
    
    dobjFor(TalkAbout)
    {        
        action()
        {
            handleTopic(&talkTopics, gIobj.topicList);
        }
    }
    
       
    
    dobjFor(SayTo)
    {        
        action()
        {
            handleTopic(&sayTopics, gIobj.topicList, &defaultSayResponse);
        }
    }
    
    defaultSayResponse = '<<pcDefaultSayQuip>><.p><<noResponseMsg>>'
    
    pcDefaultSayQuip = BMsg(default pcsayquip, 
                  '<q><<gTopicText.substr(1,1).toUpper()>><<gTopicText.substr(2).toLower()>>,</q> 
        {i} {say}. ')   
    
    
    dobjFor(QueryAbout)
    {        
        action()
        {
            handleTopic(&queryTopics, gIobj.topicList);
        }
    }
    
        
    /* The message to display if isKissable is nil */
    cannotKissMsg = BMsg(should not kiss, 'That hardly {dummy} seem{s/ed}
        appropriate. ')
    
    /*   
     *   The default response of the actor to an attempt to kiss him/her/it
     *   where this is not handled anywhere else, but allowKiss is true.
     */
    kissResponseMsg = BMsg(kiss response, '{The subj dobj} {doesn\'t like[d]}
        that. ')
    
    /*  
     *   An Actor is a more likely target of a KISS action than is an animate
     *   object.
     */
    kissRank = 100
    
    dobjFor(Kiss)
    {       
        action()
        {
            handleTopic(&miscTopics, [kissTopicObj], &kissResponseMsg);
        }
    
    }
    
    /* 
     *   By default it's normally possible to attack an actor, even if we don't
     *   want to allow it. Game code might want to override this to nil for
     *   actors it's obviously futile to try attacking, such as ghosts, gods and
     *   giants.
     */
    isAttackable = true
    
     /* 
      *   By default we'll respond to ATTACK ACTOR with the shouldNotAttackMsg;
      *   to enable responses to ATTACK via HitTopics (or some other custom
      *   handling in the action stage) set allowAttack to true.
      *
      *   If there are actors the player character will never want to attack
      *   (because their friendly or harmless, for instance) but which
      *   nevertheless could in principle could be attacked, the checkAttackMsg
      *   property can be defined (as on Thing) to stop the attack at the check
      *   stage.
      */       
    
    
    /* The message to display if isAttackable is nil */
    cannotAttackMsg = BMsg(cannot attack actor, 'That hardly {dummy} seem{s/ed}
        appropriate. ')
       
    /* 
     *   The message to display if attacking goes ahead but no HitTopics have
     *   been defined.
     */
    attackResponseMsg = cannotAttackMsg
    
    dobjFor(Attack)
    {       
        action()
        {
            handleTopic(&miscTopics, [hitTopicObj], &attackResponseMsg);
        }
    
    }
    
    dobjFor(AttackWith) asDobjFor(Attack)
    
    
    touchResponseMsg = BMsg(should not touch actor, '{The subj dobj} {doesn\'t
        appreciate[d]} being touched. ')
    
    dobjFor(Feel)
    {
        action()
        {
            handleTopic(&miscTopics, [touchTopicObj], &touchResponseMsg);
        }
    }
    
    
    
    iobjFor(GiveTo)
    {        
        action()
        {
            handleTopic(&giveTopics, [gDobj]);
        }
    }
    
    iobjFor(ShowTo)
    {        
        action()
        {
            handleTopic(&showTopics, [gDobj]);
        }
    }
    
    
    /* 
     *   Unlike inaminate objects, actors can be the logical target of a ThrowTo
     *   action
     */    
    canThrowToMe = true
    
    /* 
     *   We'll assume that actors can catch anything thrown at them by default,
     *   but game code may wish to override this assumption.
     */
    canCatchThrown(obj) { return true; }
    
      
    
    iobjFor(ThrowTo)
    {
        action()
        {
            /* 
             *   First check whether the throw is possible by checking with the
             *   Query object. This will normally only be relevant if the target
             *   actor is in a location remote from that of the thrower. If the
             *   Q object rules out the throw, move the direct object to the
             *   thrower's room and display a message saying the object fell
             *   short.
             */
            if(!Q.canThrowTo(gActor, self))
            {
                gDobj.moveInto(gActor.getOutermostRoom);
                say(throwFallsShortMsg);
                
            }            
            
            /* 
             *   Otherwise if this Actor can catch Dobj when it's thrown, move
             *   the direct object into this Actor and display an appropriate
             *   message.
             */
            else if(canCatchThrown(gDobj))
            {
                gDobj.moveInto(self);
                sayActorCatches(gDobj);
            }
            
            /* 
             *   Otherwise move the direct object into this actor's location and
             *   display a message saying that the actor dropped the catch.
             */
            else
            {
                gDobj.moveInto(location);
                sayActorDropsCatch(gDobj);   
            }
        }       
    }
    
    /* Display a message saying that this actor catches obj */
    sayActorCatches(obj)
    {
        gMessageParams(obj);
        DMsg(catch okay, '{The subj iobj} {catches} {the obj}. ');
    }
    
    /* Display a message saying that this actor failst to catch obj */
    sayActorDropsCatch(obj)
    {
        gMessageParams(obj);
        DMsg(drop catch, '{The subj iobj} fail{s/ed} to catch {the obj},
            so that {he obj} land{s/ed} on the ground instead. ');
    }
    
    dobjFor(Follow)
    {
        preCond = []
        
        verify()
        {
            /* 
             *   If the player character can see the actor s/he wants to follow,
             *   and they're in the same qocation then following the target is
             *   logical.
             */
            if(Q.canSee(gActor, self) && isIn(gActor.getOutermostRoom))
                logical;
            
            /*   But we can't follow an actor we can see in a remote location */
            else if(Q.canSee(gActor, self))
                illogicalNow(cantFollowFromHereMsg);
            
            /*  And we can't follow the actor if we don't know where it went */
            else if(lastTravelInfo == nil)
                illogicalNow(dontKnowWhereGoneMsg);
            
            /*  
             *   And we can't follow the actor if we're not in the location we
             *   last saw it depart from.
             */
            else if(!gActor.isIn(lastTravelInfo[1]))
                illogicalNow(cantStartFromHereMsg);
        }
        
        action()
        {
            /* 
             *   If we can see the actor we want to follow, then set the
             *   following Fuse (to fire at the end of this turn so we can
             *   follow the actor if it moves on this turn)
             */
            if(Q.canSee(gActor, self))
            {
                setFollowMeFuse();
            }
            
            /* 
             *   Otherwise use our stored travel information to try to follow
             *   this actor.
             */
            else if(lastTravelInfo)
            {
                /* Display a message to say we're following this actor */
                sayHeadAfterActor(lastTravelInfo[2]);
                
                /* 
                 *   Then travel via the connector this actor was seen to leave
                 *   by.
                 */
                lastTravelInfo[2].travelVia(gActor);
                
                /* 
                 *   reset the lastTravelInfo now that it's been used and is no
                 *   longer relevant.
                 */
                lastTravelInfo = nil;
            }
        }
        
    }
   
    sayHeadAfterActor(conn)
    {
        DMsg(say head after actor, '{I} head{s/ed} off {1} after {2}. ',
             conn.traversalMsg, theName);
    }
    
    waitToSeeMsg = BMsg(wait to see, '{I} wait{s/ed} to see where {he dobj}
        {goes}. ')
     

    dontKnowWhereGoneMsg = BMsg(dont know where gone, '{I} {don\'t know} where
        {the subj dobj} {has} gone. ')
    cantStartFromHereMsg = BMsg(cannot start from here, '{I}{\'m} not where {i}
        last saw {the dobj}. ')
    cantFollowFromHereMsg = BMsg(cannot follow from here, '{I} {can\'t} follow
        {him dobj} from {here}. ')
;

/*  
 *   An ActorState represents a state (possibly one of many) an actor can be in
 *   or get into. This can control how the actor is described and the actor's
 *   response to certain conversational commands and other actions.
 *
 *   ActorStates should always be located directly in the Actor to which they
 *   belong.
 */

class ActorState: EndConvBlocker, ActorTopicDatabase
    
    /* 
     *   The stateDesc from the actor's current ActorState is appended to the
     *   desc defined on the actor when the actor is described via an EXAMINE
     *   command.
     */
    stateDesc = nil
    
    /*   
     *   The specialDesc from the actor's current ActorState is used as the
     *   specialDesc for that actor in a room listing.
     */
    specialDesc = nil
    
    /*   
     *   If our associated actor is viewed from a remote location, use the
     *   ActorState's remoteSpecialDesc to describe the actor in a room listing.
     *   By default we just use the specialDesc.
     */    
    remoteSpecialDesc(pov) { specialDesc; }
    
    
    /*   
     *   Set isInitState to true if you want this ActorState to be the one the
     *   associated Actor starts out in.
     */
    isInitState = nil
    
    
    /*   Initialize this ActorState (this is actually called at preinit). */
    initializeActorState()
    {
        /*   
         *   Initialize our getActor property from the getActor property of our
         *   location, which should simply return our associated actor. This
         *   should normally never change at run-time.
         */
        getActor = location.getActor;
        
        /* 
         *   If we're our Actor's initial state and we have a getActor (our
         *   associated actor) set our actor's current state to this ActorState
         */
        if(isInitState && getActor != nil)
            getActor.curState = self;
        
       
        
        /*   Add this ActorState to our actor's list of ActorStates */
        addToActor();
               
    }
    
    /*   Add this ActorState to our actor's list of ActorStates */
    addToActor()
    {
        /* 
         *   First convert our actor's allStates property to an empty list if
         *   it's still nil
         */
        if(getActor.allStates == nil)
            getActor.allStates = [];
        
        /*   Then add ourself to our actor's list of all its ActorStates */
        getActor.allStates += self;
    }
    
    /*  
     *   The afterAction() method is called on an actor's current ActorState
     *   when the actor is in scope for the action that's just taken place. This
     *   allows game code to define state-specific reactions.
     */
    afterAction() {}
    
    
    /*  
     *   The beforeAction() method is called on an actor's current ActorState
     *   when the actor is in scope for the action that's just about to take
     *   place. This allows game code to define state-specific reactions.
     */
    beforeAction() {}
    
    /*  
     *   Display a message saying that we're following the player character from
     *   oldLoc when our actor is in this ActorState (and the actor is following
     *   the player character)
     */
    sayFollowing(oldLoc, conn)
    {
        /* Create some convenient message substitution parameters */
        local follower = getActor, pc = gPlayerChar;
        gMessageParams(follower, pc);
        
        /* Display the message */
        DMsg(state follow, '{The follower} follow{s/ed} behind {the pc}. ');
    }
    
    /*  
     *   Display a message saying that our associated actor (typically an NPC
     *   whose departure is being viewed by the player character) is departing
     *   via conn. By default we simply use our actor's sayActorDeparting(conn)
     *   method.
     */
    sayDeparting(conn) { getActor.sayActorDeparting(conn); }
    
    /* 
     *   The message to display when the player char sees this actor arriving
     *   after traveling from loc. By default we simply use our actor's
     *   sayActorArriving(fromLoc) method.
     */
    sayArriving(fromLoc) { getActor.sayActorArriving(fromLoc); }
    
    
    /*   
     *   Our associated actor. This is set to our location at preinit by our
     *   initializeActorState method.
     */
    getActor = nil
    
    
    /*   
     *   Our actor's attention span while our actor is in this ActorState. This
     *   is the number of turns the actor will wait for the player character to
     *   say something when a our actor is the player character's current
     *   conversation partner, before our actor gives up on the conversation and
     *   terminates it through 'boredom'. A value of nil (the default) means our
     *   actor is infinitely patient and will never terminate a conversation for
     *   this reason.
     */
    attentionSpan = nil
    
    /* 
     *   the arrivingTurn method is executed when an actor in this state has
     *   just followed the player char to a new location.
     */    
    arrivingTurn() { }
    
    
    
    /*
     *   Activate the state - this is called when we're about to become
     *   the active state for an actor.  We do nothing by default.
     */
    activateState(actor, oldState) { }

    /* 
     *   Deactivate the state - this is called when we're the active state
     *   for an actor, and the actor is about to switch to a new state.
     *   We do nothing by default.  
     */
    deactivateState(actor, newState) { }
    
    
    /* 
     *   The beforeTravel notification triggered when the Actor is in this
     *   ActorState and traveler is just about to travel via connector. By
     *   default we do nothing.
     */
    beforeTravel(traveler, connector) {}
        
    /* 
     *   The afterTravel notification triggered when the Actor is in this
     *   ActorState and traveler has just traveled via connector. By default we
     *   do nothing.
     */
    afterTravel(traveler, connector) {}
    
    /* Flag -- has the player character just arrived? */
    pcJustArrived = (getActor.pcArrivalTurn == gTurns)
    
        
    /*   
     *   Determine whether our actor will allow a current conversation to be
     *   terminated for reason when in this ActorState. Return true to allow the
     *   conversation to be terminated and nil otherwise. By default we simply
     *   return true. If we return nil we should also display a message
     *   explaining why we're not allowing the conversation to end.
     */
    canEndConversation(reason) { return true; }
    
    /*   
     *   The active property is used by any TopicGroups and TopicEntries located
     *   directly within us to determine whether they in turn are active.
     *   Normally there is no reason for game code to override this on an
     *   ActorState; the property is simply provided so that TopicGroups and
     *   TopicEntries can call location.active regardless of whether they're
     *   located in TopicGroups, ActorStates or Actors.
     */    
    active = (location.active)
    
    /*   
     *   If the noResponse method is defined (i.e. non-nil) then this ActorState
     *   will behave like a TADS 3 HermitActorState, i.e. the noResponse message
     *   will be displayed in response to any conversational command
     *
     *   noResponse can be either a single-quoted string, a double-quoted string
     *   or a method. If it's a string then it will be displayed instead of any
     *   TopicEntry. It it's a method it will be executed; if it then displays
     *   anything no attempt will be made to match a TopicEntry, but it if
     *   doesn't display anything the conversational command will go on to be
     *   handled as normal.
     */
    noResponse = nil
   
    /* 
     *   The getBestMatch() method is already defined on TopicDatabase, from
     *   which ActorState inherits via ActorTopicDatabase. ActorState overrides
     *   it to allow certain modifications particular to ActorState, such as the
     *   possibility that the prop parameter might be passed as either a list or
     *   a property pointer to a list property, and the need to take into
     *   account the actor's activeKeys list.
     */    
    getBestMatch(prop, requestedList)
    {
        
        local myList;
        
        /* 
         *   In the implementation of the conversation system, prop should be
         *   passed as a property pointer, but in the base TopicDatabase class
         *   the corresponding parameter is a list, so check what we have before
         *   we deal with it.
         *
         *   If prop has been passed as a property pointer, get our list from
         *   the corresponding property.
         */        
        if(dataType(prop) == TypeProp)        
            myList = self.(prop);
        
        /*  Otherwise get our list directely from the prop parameter. */
        else
            myList = valToList(prop);
        
        /* 
         *   If we have a current activeKeys list restrict the choice of topic
         *   entries to those whose convkeys overlap with it, at least at a
         *   first attempt. If that doesn't produce a match, try the normal
         *   handling. We need to do this first to ensure that we prioritize
         *   TopicEntries whose convKeys match our actor's activeKeys (which is
         *   the whole point of our actor having activeKeys).
         */
        
        if(getActor.activeKeys.length > 0)
        {
            /* 
             *   Obtain a list that is that subset of our original list where
             *   the convKeys of the TopicEntries in the list overlaps with our
             *   actor's active keys (i.e. at this stage we only want to
             *   consider TopicEntries selected by our actor's active keys)
             */
            local kList = myList.subset({x:
                                   valToList(x.convKeys).overlapsWith(getActor.activeKeys)});
            
            /*   
             *   Now find the best match that results from using the inherited
             *   handling with our restricted list
             */
            local match = inherited(kList, requestedList);
            
            /*   If we found a suitable match, return it. */
            if(match != nil)
                return match;
            
            /* 
             *   If we didn't find a match in the current state that overlaps
             *   with activeKeys, try finding one in the actor. (Not doing this
             *   would break the Conversation Nodes mechanism, quite apart from
             *   anything else). Note we can only do this is prop has been
             *   passed as a property pointer, as the method expects.
             */            
            if(dataType(prop) == TypeProp)
            {
                /* 
                 *   If prop was passed as a property pointer, obtain the list
                 *   from the corresponding property on our actor (if it wasn't
                 *   there's no need to do anything since we're already stored
                 *   it as a list)
                 */
                myList = getActor.(prop);
                
                /*   
                 *   Obtain that subset of our list that contains TopicEntries
                 *   whose convKeys overlap with our actor's activeKeys
                 .*/
                kList = myList.subset({x:
                                      valToList(x.convKeys).overlapsWith(getActor.activeKeys)});
                
                /*  
                 *   Try to find a best match using the inherited handling with
                 *   our new sub-list.
                 */
                match = inherited(kList, requestedList);
                
                /*  If we found a suitable match, return it. */
                if(match != nil)
                    return match;
                
                /* 
                 *   Restore the list to this ActorState's list of relevant
                 *   TopicEntries
                 */
                myList = self.(prop);
            }
        }
      
        /* 
         *   If we haven't found a match corresponding to our actor's
         *   activeKeys, or if our actor doesn't have any activeKeys, simply
         *   return the result of the inherited handling.
         */
        return inherited(myList, requestedList);
    }
    
    /*  
     *   Should other actors who can notionally hear the PC talking to us
     *   overhear when information is imparted to us and we're in this
     *   ActorState? I.e. should their setInform() methods be called too? By
     *   default they should.
     */
    informOverheard = true
;

/*  
 *   A TopicDatabase is an object that can contain TopicEntries and return the
 *   best match on request. ActorTopicDatabase is a specialization of
 *   TopicDatabase for use with the conversation system, and is used as a mix-in
 *   class in the list of classes from which Actor and ActorState inherit.
 */
class ActorTopicDatabase: TopicDatabase
       
    /* 
     *   The various lists of TopicEntries located within this TopicDatabase.
     *   For exampel the askTopics list would contain a list of all our
     *   AskTopics. Note that the same TopicEntry can appear in more than one
     *   list; for example an AskTellTopic would appear in both the askTopics
     *   list and the tellTopics list, and a DefaultAnyTopic would appear in all
     *   the lists apart from initiateTopics.
     */
    askTopics = []
    tellTopics = []
    sayTopics = []
    queryTopics = []
    giveTopics = []
    showTopics = []
    askForTopics = []
    talkTopics = []
    initiateTopics = []
    miscTopics = []
    commandTopics = []
    
    /* 
     *   Return a list of our listable topics, that is the topic entries located
     *   within us that should be included in a topic inventory listing because
     *   they are (a) currently reachable and (b) currently marked for listing.
     *   The resulting list forms part of the list passed to the
     *   suggestedTopicLister.
     */
    listableTopics()
    {
        /* 
         *   Start by creating a list of all the TopicEntries we contain
         *   (excluding InitiateTopics, which are never suggested because
         *   they're never a response to a conversational command).
         */
        local lst = miscTopics + askTopics + tellTopics + sayTopics +
            queryTopics + giveTopics + askForTopics + talkTopics + showTopics
            + commandTopics;
        
        /*  Note our actor. */
        local actor = getActor;
        
        /*  Remove any duplicates from our list */
        lst = lst.getUnique();
        
        /*  
         *   Form that subset of our list that contains TopicEntries that are
         *   actually listable. These are TopicEntries that meet all of the
         *   following conditions:
         
         *. 1) They define a name property (used to list them)
         *. 2) They are currently active
         *. 3) Their curiosity is not yet satisfied
         *. 4) Their curiosity has been aroused
         *. 5) They are reachable (i.e. they could potentially be triggered
         *   by a player command on the current turn).
         *
         *   Note that we deliberately leave the reachability test to last as it
         *   is the most computationally demanding.
         */
        lst = lst.subset({x: x.name!= nil && x.active && !x.curiositySatisfied 
                        && x.curiosityAroused && x.isReachable});
        
        /*  
         *   If our actor has any activeKeys, further narrow down our list to
         *   those TopicEntries whose convKeys match (i.e. overlap with) our
         *   actor's activeKeys.
         */
        if(actor.activeKeys.length > 0)
            lst = lst.subset({x: actor.activeKeys.overlapsWith(x.convKeys)});
        
        /* Return the resulting list. */
        return lst;
    }
    
    /* 
     *   Obtain the identify of any DefaultAgendaTopic contained in this
     *   database
     */
    defaultAgendaTopic = static 
                       askTopics.valWhich({x: x.ofKind(DefaultAgendaTopic)})
    
    /*  Handle an InitiateTopic */
    initiateTopic(top)
    {
       return handleTopic(&initiateTopics, [top], nil);
    }
    
    
;

/* 
 *   A TopicGroup is an object that can be used to group ActorTopicEntries that
 *   share common features such as convKeys or isActive conditions. A TopicGroup
 *   can be used anywhere an ActorTopicEntry can be used, and any
 *   ActorTopicEntries should behave just as they would if they were in the
 *   TopicGroup's container, apart from the modifications imposed by the
 *   TopicGroup.
 */
class TopicGroup: object
    
    /* 
     *   Add a topic entry to our database; since a TopicGroup isn't a
     *   TopicDatabase we simply ask our location to add it to its database. We
     *   also modify the convKeys and scoreBoost properties of any items
     *   contained in us according to our own convKeys and scoreBoost
     *   properties.
     */
    addTopic(obj)
    {
        /* Add the topic entry to our enclosing topic database */
        location.addTopic(obj);
        
        /* 
         *   For each TopicEntry located in this TopicGroup, add any convKeys
         *   defined on the TopicGroup to those defined on the individual
         *   TopicEntries
         */        
        obj.convKeys =
            valToList(obj.convKeys).appendUnique(valToList(convKeys));
        
     }
      
    
    /*  
     *   A TopicGroup's isActive property can be used to make all the
     *   TopicEntries enclosed within in inactive by being set to nil; if it is
     *   true then the enclosed TopicEntries are active if their own isActive
     *   property is true.
     */
    isActive = true
    
    /* 
     *   This TopicGroup is active if both its own isActive property is true and
     *   its location is active (this allows us to locate TopicGroups within
     *   other TopicGroups, for instance)
     */
    active = (isActive && location.active)
    
    /*  
     *   A list of convKeys that should be added to the convKeys of each of our
     *   TopicEntries.
     */
    convKeys = nil
    
    /*   
     *   A scoreBoost that should be added to the scoreBoost of each of our
     *   TopicEntries.
     */
    scoreBoost = 0
    
    /*  
     *   By how much do we boost the score of any TopicEntries we contain? We
     *   need to add our own score boost to that our own location.
     */
    scoreBooster()
    {
        local sb = scoreBoost;
        
        if(location.propDefined(&scoreBooster))
            sb += location.scoreBooster();
        
        return sb;
    }
    
    
    /*   
     *   If we're being used as a conversation node, our node is active when our
     *   own convKeys matches (i.e. overlaps with) that of our actor's
     *   activeKeys.
     */
    nodeActive()
    {
        return valToList(convKeys).overlapsWith(getActor.activeKeys);
    }
    
    /* Our associated actor is our location's associated actor. */
    getActor = (location.getActor)    
;

/* 
 *   A ConvNode is a TopicGroup specialized for use as a ConversationNode; it's
 *   active when its nodeActive property is true.
 */

class ConvNode: TopicGroup
    isActive = nodeActive
    
    /* Register this ConvNode with its associated actor. */
    register()
    {
        /* Get our asociated actor. */
        local actor = getActor;
        
        /* Add to our actor's list of ConvNodes. */
        actor.allNodes = actor.allNodes.append([convKeys, self]);
    }
;

convNodePreinit: PreinitObject
    execute()
    {
        forEachInstance(ConvNode, {c: c.register() });
    }
;

/* 
 *   An ActorTopicEntry is a specialization of TopicEntry for use with the
 *   conversation system. ActorTopicEntries represent potential responses to
 *   conversational commands like ASK BOB ABOUT LIGHTHOUSE or TELL GEORGE ABOUT
 *   FIRE.
 *
 *   Since ActorTopicEntry inherits from ReplaceRedirector as well as
 *   TopicEntry, its topicResponse() methods can make use of doInstead() and
 *   doNested().
 */
class ActorTopicEntry: ReplaceRedirector, TopicEntry
    
    /* 
     *   To make this a suggested topic, just give it a name under which it will
     *   be suggested (of the kind that could follow 'You could ask about ' or
     *   'You could tell him about ' or 'You could show him ' etc.). Note that
     *   for QueryTopics and SayTopics that are specified with a matchObj the
     *   library constructs this name automatically.
     */         
    name = nil
    
    /* 
     *   Set autoName to true to have this topic entry generate a name from its
     *   first matchObj's theName     */
    
    autoName = nil
    
    /*   
     *   An ActorTopicEntry is conversational (the default) if it results in an
     *   actual conversational exchange. Change this to nil for
     *   ActorTopicEntries that merely report why a conversational exchange did
     *   not take place (e.g. "Bob ignores you" or "You think better of talking
     *   to George about that.")
     */
    isConversational = true
    
    /*  
     *   Normally a conversational command implies a greeting (that is, it
     *   should trigger a greeting if a conversation is not already in process).
     *   This needs to be overridden to nil on ActorTopicEntries that explicitly
     *   handle greetings (HelloTopic and its subclasses) to avoid an infinite
     *   loop.
     */
    impliesGreeting = isConversational
    
    /* 
     *   A string or list of strings defining one or more groups to which this
     *   topic entry belongs. Under certain circumstances an ActorTopicEntry may
     *   be prioritized if its convKeys overlaps with the associated actor's
     *   activeKeys.
     */
    convKeys = nil
    
    /* 
     *   Test whether any of this Topic Entry's convKeys match those in the
     *   Actor's activeKeys list (whether or not the latter has any entries).
     *   This can be used in the isActive property to make this topic entry
     *   *only* available when its convKeys are active.
     */    
    nodeActive()
    {
        return valToList(convKeys).overlapsWith(getActor.activeKeys);
    }
    
    /* Add this ActorTopicEntry to its associated actor's table of convKeys */
    addToConvKeyTable()   
    {
        /* Note our associated actor. */
        local actor = getActor;
        
        /* If our actor doesn't yet have a convKeyTab, create one */
        if(actor.convKeyTab == nil)
            actor.convKeyTab = new LookupTable;
        
        /*  
         *   Go through every key in our convKeys list and add this
         *   ActorTopicEntry to the list of ActorTopicEntries that correspond to
         *   it in our actor's convKeyList.
         */
        foreach(local k in valToList(convKeys))
        {
            /* Obtain the existing value corresponding to this key */
            local val = actor.convKeyTab[k];
            
            /* 
             *   Make sure the value is a list, and then add this
             *   ActorTopicEntry to it before storing it in the table.
             */
            actor.convKeyTab[k] = valToList(val) + self;
        }
    }
    
    /* Initialize this ActorTopicEntry (this is actually called at preinit) */
    initializeTopicEntry()
    {
        /* Carry out the inherited handling (on TopicEntry) */
        inherited;
        
        /* 
         *   Add this ActorTopicEntry and its associated convKeys to our actor's
         *   convKeyTable
         */
        addToConvKeyTable();
        
        /*  
         *   If our autoname property is true, construct our name (for use in
         *   suggesting this TopicEntry) provided we have something to construct
         *   it from.
         */
        if(autoName && matchObj != nil && name is in (nil, ''))
            buildName();
    }
    
    /* 
     *   Construct the name of this ActorTopicEntry by using the theName
     *   property of our first matchObj.     
     */
    buildName() { name = valToList(matchObj)[1].theName; }
    
    /* Our associated actor is our location's associated actor. */
    getActor = (location.getActor)
    
    /* 
     *   The number of times to suggest this topic entry, if we do suggest it.
     *   By default this is either once (if we're not also an EventList) or the
     *   number of items in our eventList (if we are an EventList). If you want
     *   this topic entry to go on being suggested ad infinitum, set
     *   timesToSuggest to nil.
     */    
    timesToSuggest()
    {
        local t = (ofKind(Script) ? eventList.length : 1);
        
        timesToSuggest = t;
        
        return t;        
    }
    
    /* 
     *   Assuming this topic entry is ever suggested, it will continue to be
     *   suggested until curiositySatisfied becomes true. By default this occurs
     *   when the topic has been invoked timesToSuggest times. If, however, we
     *   have any keyTopics we'll take our curiosity to be satisfied when our
     *   keyTopics have all been satisfied.
     */
    curiositySatisfied()
    {
        if(keyTopics == nil)
            return( timesToSuggest != nil && timesInvoked >= timesToSuggest);
        else
            return getKeyTopics(nil).length == 0;
    }
    
    /* The number of times this topic entry has been invoked. */
    timesInvoked = 0
    
    
    /* 
     *   We won't suggest this topic entry (if we ever suggest it at all) until
     *   its curiosityAroused property by true. By default it normally is from
     *   the start, but this can be overridden in individual cases if desired.
     */
    curiosityAroused = true
    
    /* 
     *   The suggestAs property can be overridden to change the list this topic
     *   entry will be suggested as if you don't want it placed in the list the
     *   library chooses by default. The allowed values are one of AskTopic,
     *   TellTopic, GiveTopic, ShowTopic, or TalkTopic. Normally, though, the
     *   library default will work perfectly well and you don't need to specify
     *   this property. If you do it must be specified as a kind that this topic
     *   entry can actually match, e.g. TellTopic for an AskTellTopic or
     *   ShowTopic for a GiveShowTopic.
     */    
    suggestAs = nil
    
    /*   
     *   The order this TopicEntry appears in within its group in a list of
     *   suggested topics. The higher this number, the later this topic will be
     *   suggested. Note that this does not override the grouping of suggestions
     *   into AskTopics, TellTopics, etc.
     */
    listOrder = 100
    
    /*   
     *   Handle this topic (if we're the ActorTopicEntry selected to respond to
     *   a conversational command.
     */
    handleTopic()
    {
        /* Increment our timesInvoked counter */            
        timesInvoked++ ;
        
        /* 
         *   If we have a list of keyTopics defined, then we display them as a
         *   list of topic suggestions instead of showing our topicResponse.
         *   This allows one topicEntry to be used as a means of suggesting
         *   other, more specific, topics.
         */
        if(valToList(keyTopics).length > 0)
        {
            /* Show a list of our keyTopics */
            showKeyTopics();
            
            /* 
             *   Throw an abort signal so that showing a list of topics doesn't
             *   count as a player turn.
             */
            abort;
        }
        
        /* Otherwise execute our topicResponse  */
        else
            topicResponse();
    }
        
    /* 
     *   The keyTopics can contain a convKey or a list of convKeys, in which
     *   case when this TopicEntry is triggered instead of responding directly
     *   it will list topic suggestions that correspond to the convKeys defined
     *   here. For example, a TopicEntry that responded to ASK BOB ABOUT
     *   TROUBLES could define a keyTopics property of 'troubles' that triggered
     *   more specific suggestions such as "You could ask when the troubles
     *   started, or what the troubles were, or how the troubles ended",
     *   assuming that these QueryTopics had a convKeys property of 'troubles'
     *
     *   If you want this TopicEntry to display its topicResponse in the normal
     *   way, leave keyTopics as nil.
     */
    keyTopics = nil
    
    /* Show our suggested keyTopics, if keyTopics is defined. */
    showKeyTopics()
    {
        /* 
         *   First construct a list of TopicEntries that match the keys in our
         *   keyTopics.
         */
        local lst = getKeyTopics(true);
        
        /*   
         *   If the list contains any entries, display the list of suggestions
         *   using the suggestedTopicLister
         */
        if(lst.length > 0)                    
           suggestedTopicLister.show(lst);       
        
        /* 
         *   Otherwise display a message explaining that we've nothing to
         *   discuss on this topic.
         */
        else
            DMsg(nothing to discuss on that topic, '{I} {have} nothing to
                discuss on that topic just {then}. '); 
    }
    
    /* Obtain a list of the TopicEntries that match our keyTopics property. */
    getKeyTopics(updateStatus)
    {
        /* Make a note of our associated actor. */
        local actor = getActor();

        /* Initialize an empty list */
        local lst = [];
        
        /* 
         *   For each key value in our keyTopics list, look up the associated
         *   TopicEntries in our actor's convKeyTab and add them to our list. If
         *   however a key value looks like a <. > tag, output the tag (this
         *   could be use to activate or arouse a group of topics just prior to
         *   suggesting them).
         */
        foreach(local ky in valToList(keyTopics))
        {
            /* 
             *   If this value looks like a control tag, output it straight
             *   away.
             */
            if(ky.startsWith('<.'))
            {
                if(updateStatus)
                    say(ky.trim);
            }
            /* Otherwise add it to our list. */
            else                
                lst += actor.convKeyTab[ky];
        }
        
        /*   
         *   Reduce our list to a subset that only contains those TopicEntries
         *   that (1) are active, (2) don't yet have their curiosity satisfied,
         *   (3) have their curiosity aroused and (4) are reachable (i.e. they
         *   would actually be triggered if the player were to follow the
         *   suggestion). If we're trying simply to determine whether this
         *   TopicEntry should be suggested (i.e. when updateStatus = nil) we
         *   need to use slightly less stringent criteria to determine the list
         *   of items that we might key, since our keyed items may not have been
         *   activated yet.
         */
        if(updateStatus)
            lst = lst.subset({t: t.active && !t.curiositySatisfied &&
                             t.curiosityAroused && t.isReachable });
        else
            lst = lst.subset({t:t.isActive && !t.curiositySatisfied});
            
        /* Remove any duplicate entries from the list. */
        lst = nilToList(lst).getUnique();
        
        /* Return the list. */
        return lst;
    }
       
    
    /* 
     *   A flag that can be set with an <.activate> tag. It must be true for
     *   this TopicEntry to be active, regardless of the value of isActive. It
     *   starts out true by default, but it can be set to nil on TopicEntries
     *   that you want to start out as inactive subsequently activate via an
     *   activate tag.
     */
    activated = true
    
    
    /* 
     *   Activate this TopicEntry. This would normally be called in game code
     *   via an <.activate> tag. 
     */
    activate() { activated = true; }
    
    /*  
     *   Deactivate this topic. This could typically be used from within the
     *   topicResponse of an ActorTopicEntry you only want to use once (or in
     *   the last entry in a StopEventList of an ActorTopicEntry). It can also
     *   be called via a <.deactivate key> tag in combination with the convKeys.
     */
    deactivate() { activated = nil; }
    
    /* 
     *   This TopicEntry is active if its own isActive property is true and its
     *   activated property is true and if its location is active. This allows
     *   the isActive conditions of individual TopicEntries to be combined with
     *   that of any TopicGroups they're in. This property should not normally
     *   be overridden in game code.
     */
    active = (isActive && activated && location.active)
    
    
    /* 
     *   Determine whether this TopicEntry is currently reachable, i.e. whether
     *   it could be reached if the player asked/told etc. about its matchObj on
     *   the next turn.
     */
    isReachable()
    {
        /* Note our associated actor */
        local actor = getActor;
        
        /* 
         *   If the actor has a current ActorState and we're in a different
         *   ActorState then we're reachable only if we're in the current
         *   ActorState
         */        
        if(actor.curState != nil && location.ofKind(ActorState) 
           && location != actor.curState)
            return nil;
        
                    
        /* 
         *   If we don't have a matchObj (or our matchObj is an Action) assume
         *   we're reachable unless certain conditions apply (e.g. we're blocked
         *   by a DefaultTopic).
         */        
        if(matchObj == nil || matchObj.ofKind(Action))
        {
            /* 
             *   if the actor doesn't have a current actor state or we're in the
             *   current actor state, assume we're reachable
             */            
            if(actor.curState == nil || location == actor.curState)            
               return true;
            
            /* 
             *   Otherwise, we're reachable if the current actor state doesn't
             *   have a DefaulTopic that might block us, or if our convKeys
             *   overlap with that of the actor's activeKeys
             */
            
            /*   First check if we're reachable by virtue of our convKeys */
            if(valToList(convKeys).overlapsWith(getActor.activeKeys))
                return true;
            
            
            /*   
             *   Then check for a DefaultTopic in the Actor's current ActorState
             */
            foreach(local prop in includeInList)
            {
                if(actor.curState.(prop).indexWhich({ t: t.ofKind(DefaultTopic)
                    }) != nil)
                    return nil;
            }
            
            /* 
             *   There's nothing obvious that makes this TopicEntry unreachable,
             *   so return true to say we are reachable.
             */
            return true;
            
        }
        
        /* 
         *   We're not reachable if the player char doesn't know about our
         *   matchObj
         */
        
        if(valToList(matchObj).indexWhich({ x: x.isClass() 
                                          || gPlayerChar.knowsAbout(x)}) == nil)         
            return nil;
        
        /* 
         *   Otherwise we're reachable if we're the best match for our matchObj
         *   according to our suggestion type
         */
        
        /* 
         *   If the author has indicated a suggestAs property, use it to
         *   determine which list property we should test for, otherwise use the
         *   first one in our own list, which should correspond to the topic
         *   suggestion lister's default behaviour. By this means we simulate
         *   the command the sugggestion lister will suggest; e.g. if it would
         *   suggest ASK ABOUT FOO we test whether this topic entry is reachable
         *   via an ASK ABOUT command, so we want to test whether its the best
         *   response for its matchObj from the askTopic list.
         */
        
        /*   
         *   Find the topic entry list property of our TopicDatabase that would
         *   be searched to find us if the player followed the suggestion to try
         *   us (e.g. if we would be listed as "You could ask about foo" prop
         *   should come out as &askTopics).
         */
        local prop = (suggestAs != nil ? suggestAs.includeInList[1] :
                      includeInList[1]);
        
        /*   
         *   If we'd be sought as a QueryTopic, determine what qType we'd match;
         *   if we can match more than one qType, select our first one for this
         *   exercise.
         */             
        if(prop == &queryTopics)
            gAction.qType = qtype.split('|')[1];
        
        /* 
         *   Try seeing what the best response would be if we asked our actor to
         *   find the best matching TopicEntry for our matchObj in its prop list
         *   (e.g. its askTopics list if prop is &askTopics). If the result is
         *   this TopicEntry, then this TopicEntry is reachable, so return true.
         */
        if(actor.findBestResponse(prop, matchObj) == self)
            return true;
        
        /*   
         *   Otherwise it's not reachable, so return nil. (This might happen if
         *   another matching topic has a higher matchScore, for example).
         */
        return nil;           
            
    }
;
 

/* 
 *   CommandTopicHelper is a mix-in class for use with CommandTopic and
 *   DefaultCommantTopic to provide some common handling for both. Its base
 *   class LCommandTopicHelper (which provides a method for reconstructing the
 *   text of a command issued to an actor) must be defined in the
 *   language-specific part of the library.
 */
class CommandTopicHelper: LCommandTopicHelper
    handleTopic()
    {
        /* Carry out the inherited handling */
        inherited;
        
        /* 
         *   If this CommandTopic allows the action our actor has been ordered
         *   to carry out to proceed, then execute it
         */
        if(allowAction)
            myAction.exec(gCommand);
    }
    
    /* 
     *   Set this to true to allow the action to proceed as commanded by the
     *   player.
     */
    allowAction = nil
        
    /*   
     *   The action our actor has been ordered to carry out, which will be the
     *   action on the current Command object.
     */
    myAction = (gCommand.action)
;

/*  
 *   A CommandTopic is a TopicEntry that handles a command directed at this
 *   actor (e.g. BOB, JUMP).
 */
class CommandTopic: CommandTopicHelper, ActorTopicEntry    
    matchTopic(top)
    {
               
        myAction = top;
        
        /* 
         *   If we've specified that this CommandTopic must match specific
         *   objects and the action we've been passed doesn't match them, return
         *   nil.
         */
        if(matchDobj != nil && (top.curDobj == nil ||
           valToList(matchDobj).indexWhich ({x: top.curDobj.ofKind(x)}) == nil))
            return nil;
        
        if(matchIobj != nil && (top.curIobj == nil ||
           valToList(matchIobj).indexWhich ({x: top.curIobj.ofKind(x)}) == nil))
            return nil;
     
        /* return the inherited handling */
        return inherited(top);
        
        
    }
    
    
    /* 
     *   The direct and indirect objects I must match (individually or as one of
     *   a list) if this CommandTopic is to be matched.
     */
    matchDobj = nil
    matchIobj = nil
    
    /* the action I've just matched. */
    myAction = nil
    
    
    /* 
     *   CommandTopics are included in the commandTopics list of their
     *   ActorTopicDatabase
     */
    includeInList = [&commandTopics]
    
    
;

/* 
 *   A MiscTopic is an ActorTopicEntry that responds not to a conversational
 *   command specifying a separate topic (such as ASK BOB ABOUT FRUIT) but just
 *   to as simple command like YES, NO, HELLO or GOODBYE
 */
class MiscTopic: ActorTopicEntry
    /* 
     *   A MiscTopic isn't matched to a topic in the normal sense, but we
     *   instead pass the routine an obj parameter to determine what particular
     *   kind of MiscTopic (e.g. YesTopic or ByeTopic) we want to match.
     */    
    matchTopic(obj)
    {
        /* Note the topic we're trying to match. */
        topicMatched = obj;
        
        /* 
         *   if it's one of our matching topics, return our match score,
         *   otherwise return a nil score to indicate failure 
         */
        return (valToList(matchObj).indexOf(obj) != nil) ? matchScore +
            scoreBooster(): nil;
    }
;

/*  
 *   A KissTopic can be used to provide a response to KISS ACTOR, provided that
 *   we have overridden allowKiss to true on the actor object. This allows the
 *   response to Kiss to vary according to ActorState or other conditions in a a
 *   way that can readily be expressed in a declarative programming style
 */
class KissTopic: MiscTopic
    /* 
     *   KissTopics should be included in the miscTopics list of their
     *   TopicDatabase (Actor or ActorState)
     */
    includeInList = [&miscTopics]
    
    /*   A KissTopic matches the kissTopicObj */
    matchObj = kissTopicObj
    
    /*   
     *   A KissTopic is not regarded as conversational, since KISS ACTOR is not
     *   normally treated as a conversational exchange.
     */
    isConversational = nil
    
    /*   Kissing someone should not trigger a greeting */
    impliesGreeting = nil
;

/*  The kissTopicObject is simply an object used for KissTopic to match. */
kissTopicObj: object;


/*  
 *   A HitTopic can be used to respond to HIT ACTOR (or ATTACK ACTOR, etc.),
 *   provided the actor's allowAtack property has been overridden to true
 */
class HitTopic: MiscTopic
    /* 
     *   HitTopics should be included in the miscTopics list of their
     *   TopicDatabase (Actor or ActorState)
     */
    includeInList = [&miscTopics]
    
    /* HitTopics match the hitTopicObj */
    matchObj = hitTopicObj
    
    /* 
     *   Hitting someone is not normally regarded as form of conversational
     *   exchange.
     */
    isConversational = nil
    
    /*  Hitting someone does not trigger a greeting */
    impliesGreeting = nil
;

/* The hitTopicObj exists solely as something for HitTopics to match. */
hitTopicObj: object;

class TouchTopic: MiscTopic
    /* 
     *   TouchTopics should be included in the miscTopics list of their
     *   TopicDatabase (Actor or ActorState)
     */
    includeInList = [&miscTopics]
    
    /* TouchTopics match the hitTopicObj */
    matchObj = touchTopicObj
    
    /* 
     *   Touching someone is not normally regarded as form of conversational
     *   exchange.
     */
    isConversational = nil
    
    /*  Touching someone does not trigger a greeting */
    impliesGreeting = nil
;

/* The touchTopicObj exists solely as something for HitTopics to match. */
touchTopicObj: object;

/* A YesTopic is a TopicEntry that responds to YES or SAY YES */
class YesTopic: MiscTopic
    /* YesTopics are included in the miscTopics list of their TopicDatabase */
    includeInList = [&miscTopics]
    
    /* YesTopics match the yesTopicObj */
    matchObj = yesTopicObj
    
    /* 
     *   We give YesTopic a name so that it can be suggested in response to a
     *   request to display a list of suggested topics.
     */
    name = BMsg(say yes, 'say yes')
;

/* A NoTopic is a TopicEntry that responds to NO or SAY NO */
class NoTopic: MiscTopic
    /* NoTopics are included in the miscTopics list of their TopicDatabase */    
    includeInList = [&miscTopics]
    
    /* NoTopics match the noTopicObj */
    matchObj = noTopicObj
    
    /* 
     *   We give NoTopic a name so that it can be suggested in response to a
     *   request to display a list of suggested topics.
     */
    name = BMsg(say no, 'say no')
    
    /* 
     *   We define the suggestAs property of the NoTopic so that the
     *   suggestedTopicLister can readily separate it from YesTopics
     */
    suggestAs = NoTopic
;

/* A YesNoTopic is a TopicEntry that responds to either YES or NO */
class YesNoTopic: MiscTopic
        /* 
         *   YesNoTopics are included in the miscTopics list of their
         *   TopicDatabase
         */
    includeInList = [&miscTopics]
    
    /* YesNoTopics match the yesTopicObj or the noTopicObj*/
    matchObj = [yesTopicObj, noTopicObj]
    
    /* 
     *   We give YesNoTopic a name so that it can be suggested in response to a
     *   request to display a list of suggested topics.
     */
    name = BMsg(say yes or no, 'say yes or no')
;

/*  
 *   A GreetingTopic is a kind of TopicEntry used in greeting protocols (saying
 *   Hello or Goodbye). Game code will not use this class directly but will
 *   instead use one or more of its subclasses
 */
class GreetingTopic: MiscTopic
    includeInList = [&miscTopics]
    impliesGreeting = nil
    
    /* 
     *   It may be that we want to change to a different actor state when we
     *   begin or end a conversation. If so the changeToState property can be
     *   used to specify which state to change to.
     */
    changeToState = nil
    
    /*   
     *   Handling a GreetingTopic includes the requested state change, if
     *   changeToState is defined
     */
    handleTopic()
    {
        /* 
         *   Carry out the inherited handling and store the result (true or nil
         *   for success or failure)
         */
        local result = inherited();
        
        /*  
         *   If changeToState is not nil, change our actor's current ActorState
         *   accordingly
         */
        if(changeToState != nil)
            getActor.setState(changeToState);        
        
        /* Return the result of the inherited handling. */
        return result;
    }
;

/* 
 *   A HelloTopic is a TopicEntry that handles an explicit greeting (the player
 *   character explicitly saying Hello to this actor). It also handles implicit
 *   greetings (triggered when the player enters a conversational command when a
 *   conversation with this actor is not already going on), unless we have also
 *   defined an ImpHelloTopic, which will then take preference.
 */
class HelloTopic: GreetingTopic    
    /* A HelloTopic matches either helloTopicObj or impHelloTopicObj */
    matchObj = [helloTopicObj, impHelloTopicObj]    
    
    handleTopic()    
    {
        /* 
         *   Activate our actor's pending agenda items at the start of this new
         *   conversation.
         */
        getActor.activatePendingAgenda();
        
        /* Carry out the inherited handling and return the result. */
        return inherited;
    }
;

/* 
 *   An ImpHelloTopic is one that handles an implied greeting; i.e. it is used
 *   to start a conversation when some other conversational command is used
 *   before the conversation is underway.
 */
class ImpHelloTopic: HelloTopic
    /* An ImpHelloTopic matches the impHelloTopicObj only. */
    matchObj = [impHelloTopicObj]
    
    /* 
     *   We give ImpHelloTopic a higher than usual matchScore so that it's used
     *   in preference to a HelloTopic when both are present to match the
     *   impHelloTopicObj.
     */
    matchScore = 150
;

/*
 *   Actor Hello topic - this handles greetings when an NPC initiates the
 *   conversation. 
 */
class ActorHelloTopic: HelloTopic    
    /* An ActorHelloTopic matches the actorHelloTopicObj only. */
    matchObj = [actorHelloTopicObj]
    
    matchScore = 200


    /* 
     *   If we use this as a greeting upon entering a ConvNode, we'll want
     *   to stay in the node afterward
     */
    noteInvocation(fromActor)
    {
        /* Carry out the inherited handling. */
        inherited(fromActor);
        
        /* Issue a constay tag */-
        "<.convstay>";
    }
;

/*
 *   A goodbye topic - this handles both explicit GOODBYE commands and
 *   implied goodbyes.  Implied goodbyes happen when a conversation ends
 *   without an explicit GOODBYE command, such as when the player character
 *   walks away from the NPC, or the NPC gets bored and wanders off, or the
 *   NPC terminates the conversation of its own volition.  
 */
class ByeTopic: GreetingTopic    
    /* 
     *   This most general kind of ByeTopic matches every kind of
     *   conversation-ending object
     */
    matchObj = [endConvBye,
                 endConvLeave, endConvBoredom, endConvActor]

    
;

/* 
 *   An implied goodbye topic.  This handles ONLY automatic (implied)
 *   conversation endings, which happen when we walk away from an actor
 *   we're talking to, or the other actor ends the conversation after being
 *   ignored for too long, or the other actor ends the conversation of its
 *   own volition via npc.endConversation().
 *   
 *   We use a higher-than-default matchScore so that any time we have both
 *   a ByeTopic and an ImpByeTopic that are both active, we'll choose the
 *   more specific ImpByeTopic.  
 */
class ImpByeTopic: GreetingTopic    
    /* 
     *   The ImpByeTopic matches endConvLeave, endConvBoredom, or endConvActor
     *   (but not endConvBye).
     */
    matchObj = [endConvLeave, endConvBoredom, endConvActor]
    
    /* 
     *   Give ImpByeTopic a high matchScore so that it takes precedence over
     *   ByeTopic when both are present.
     */
    matchScore = 200
;

/*
 *   A "bored" goodbye topic.  This handles ONLY goodbyes that happen when
 *   the actor we're talking terminates the conversation out of boredom
 *   (i.e., after a period of inactivity in the conversation).
 *   
 *   Note that this is a subset of ImpByeTopic - ImpByeTopic handles
 *   "bored" and "leaving" goodbyes, while this one handles only the
 *   "bored" goodbyes.  You can use this kind of topic if you want to
 *   differentiate the responses to "bored" and "leaving" conversation
 *   endings.  
 */
class BoredByeTopic: GreetingTopic    
    /* A BoredByeTopic matches endConvBoredom only */
    matchObj = [endConvBoredom]
    
    /* 
     *   Give BoredByeTopic an even higher matchScore so that it takes
     *   precedence over ImpByeTopic when both are present.
     */
    matchScore = 300
;

/*
 *   A "leaving" goodbye topic.  This handles ONLY goodbyes that happen
 *   when the PC walks away from the actor they're talking to.
 *   
 *   Note that this is a subset of ImpByeTopic - ImpByeTopic handles
 *   "bored" and "leaving" goodbyes, while this one handles only the
 *   "leaving" goodbyes.  You can use this kind of topic if you want to
 *   differentiate the responses to "bored" and "leaving" conversation
 *   endings.  
 */
class LeaveByeTopic: GreetingTopic    
    /* A LeaveByeTopic matches endConvLeave only */
    matchObj = [endConvLeave]
    
    /* 
     *   Give LeaveByeTopic an even higher matchScore so that it takes
     *   precedence over ImpByeTopic when both are present.
     */
    matchScore = 300
;

/*
 *   An "actor" goodbye topic.  This handles ONLY goodbyes that happen when
 *   the NPC terminates the conversation of its own volition via
 *   npc.endConversation(). 
 */
class ActorByeTopic: GreetingTopic   
    /* An ActorByeTopic matches endConvActor only */
    matchObj = [endConvActor]
    
    /* 
     *   Give BoredByeTopic an even higher matchScore so that it takes
     *   precedence over ImpByeTopic when both are present.
     */
    matchScore = 300
;

/* a topic for both HELLO and GOODBYE */
class HelloGoodbyeTopic: GreetingTopic    
    /* A HelloGoodbyeTopic matches every kind of hello and endConv object */
    matchObj = [helloTopicObj, impHelloTopicObj,
                 endConvBye, endConvBoredom, endConvLeave,
                 endConvActor]
    
    /* 
     *   We give HelloGoodByeTopic a slightly lower than normal matchScore to
     *   ensure that all the other, more specific, types of HelloTopic and
     *   ByeTopics take precedence over it.
     */
    matchScore = 90
;

/* 
 *   Topic singletons representing HELLO and GOODBYE topics.  These are
 *   used as the parameter to matchTopic() when we're looking for the
 *   response to the corresponding verbs. 
 */
helloTopicObj: object;
endConvBye: object;

/* 
 *   a topic singleton for implied greetings (the kind of greeting that
 *   happens when we jump right into a conversation with a command like
 *   ASK ABOUT or TELL ABOUT, rather than explicitly saying HELLO first) 
 */
impHelloTopicObj: object;

/*
 *   a topic singleton for an NPC-initiated hello (this is the kind of
 *   greeting that happens when the NPC is the one who initiates the
 *   conversation, via actor.initiateConversation()) 
 */
actorHelloTopicObj: object;


/* 
 *   topic singletons for the two kinds of automatic goodbyes (the kind of
 *   conversation ending that happens when we simply walk away from an
 *   actor we're in conversation with, or when we ignore the other actor
 *   for enough turns that the actor gets bored and ends the conversation
 *   of its own volition) 
 */
endConvBoredom: object;
endConvLeave: object;

/*
 *   a topic singleton for an NPC-initiated goodbye (this is the kind of
 *   goodbye that happens when the NPC is the one who breaks off the
 *   conversation, via npc.endConversation()) 
 */
endConvActor: object;


/* 
 *   A DefaultTopic is a kind of TopicEntry for use as a fallback when the
 *   player attempts to discuss a topic that game code doesn't explicitly cater
 *   for.
 */
class DefaultTopic: ActorTopicEntry       
    /* A DefaultTopic matches any Thing or Topic or yes or no */
    matchObj = [Thing, Topic, yesTopicObj, noTopicObj]
    
    /* 
     *   A DefaultTopic has a very low matchScore to allow anything more
     *   specific to take precedence.
     */
    matchScore = 1    
    
    
    /* 
     *   Don't match this DefaultTopic if top is one of the topics we want to
     *   avoid matching. Otherwise carry out the inherited handling.
     */
    matchTopic(top)
    {
        if(avoidMatching(top))
            return nil;
        else
            return inherited(top);
    }
    
    /* 
     *   A list of topics we don't want this DefaultTopic to match, so matching
     *   can fall through to another topic database to handle it (thereby
     *   facilitating the common handling of some topics across ActorStates).
     */
    
    exceptions = []
    
    /* 
     *   Do we want to avoid this DefaultTopic matching top, so that it can be
     *   matched elsewhere? By default we do so if top is listed in our
     *   exceptions.
     */
    
    avoidMatching(top)
    {
        return (valToList(exceptions).indexOf(top) != nil);
    }
;

/* 
 *   A DefaultAnyTopic is a DefaultTopic that can match any kind of
 *   conversational command, except for HELLO and GOODBYE (unless matchGreetings
 *   is true.
 */
 
class DefaultAnyTopic: DefaultTopic
    /* 
     *   DefaultAnyTopics are included in all the lists of their TopicDatabase
     *   that contain lists of conversational responses.
     */
    includeInList = [&sayTopics, &queryTopics, &askTopics, &tellTopics,
        &giveTopics, &showTopics, &askForTopics, &talkTopics, &miscTopics,
    &commandTopics]
    
    /* 
     *   A DefaultAnyTopic matches any conversational command and should also
     *   match Commands (where the matchObj will be an action); they can
     *   optionally match HELLO and BYE as well.
     */
    matchObj = inherited + Action + (matchGreetings ? [helloTopicObj,
        endConvBye] : [])    
 
    /* 
     *   Flag - should we match HELLO and BYE? By default we don't since many
     *   DefaultAnyTopic responses won't be suitable for this purpose, but this
     *   can be overridden to true for DefaultAnyTopics that should match HELLO
     *   and BYE as well as other conversational commands.
     */
    matchGreetings = nil
;

/* 
 *   A DefaultAnyNonSayTopic matches any conversational command except SAY and should also match
 *   Commands (where the matchObj will be an action); they can optionally match HELLO and BYE as
 *   well. */

class DefaultAnyNonSayTopic: DefaultAnyTopic
    includeInList = [&queryTopics, &askTopics, &tellTopics,
        &giveTopics, &showTopics, &askForTopics, &talkTopics, &miscTopics,
    &commandTopics]
    
    matchScore = 2
;


/* 
 *   A DefaultAgendaTopic can be used to give the actor the opportunity to seize
 *   the conversational initiative when the player enters a conversational
 *   command for which there's no explicit match. Instead of giving a bland
 *   default response the actor can instead respond with an item from its own
 *   agenda, e.g. "Never mind that, what I really want to know is...".
 *
 *   Items can be added to the agenda of a DefaultAgendaTopic by calling its
 *   addToAgenda method. To obtain a reference to a DefaultAgendaTopic use the
 *   defaultAgendaTopic property of the Actor or ActorState in which it is
 *   located (note, therefore, that there should only be at most one of these
 *   per Actor or Actor State).
 *
 *   Note that you should define the topicResponse or eventList property of a
 *   DefaultAgendaTopic in case none of the agenda items in its agenda list turn
 *   out to be executable.
 */
class DefaultAgendaTopic: AgendaManager, DefaultAnyTopic
    
    handleTopic()
    {
        /* 
         *   Try to execute our next agenda item. If this fails fall back on our
         *   inherited handling.
         */        
        if(!executeAgenda())
            inherited();
    }
    
    /* 
     *   This kind of Default Topic is active only when it has any agenda items
     *   to process.
     */   
    active = inherited && agendaList != nil && 
           agendaList.indexWhich({x: x.isReady})
    
    /* 
     *   When this DefaultTopic is active we want it to take priority over over
     *   DefaultTopics.
     */
    matchScore = 10
;

/* 
 *   A DefaultConversationTopic is a DefaultTopic that matches any strictly
 *   conversational command; it matches everything a DefaultAnyTopic matches
 *   apart from GIVE and SHOW (which don't necessarily imply verbal exchanges)
 */
class DefaultConversationTopic: DefaultTopic
    includeInList = [&sayTopics, &queryTopics, &askTopics, &tellTopics,
        &askForTopics, &talkTopics]
    matchScore = 2
;

/* 
 *   A DefaultConversationTopic is a DefaultConverationTopic that matches any strictly
 *   conversational command apart from SAY.
 */
class DefaultNonSayTopic: DefaultConversationTopic
    includeInList = [&queryTopics, &askTopics, &tellTopics,
        &askForTopics, &talkTopics]
    
    matchScore = 3
;

/* Default Topic to match ASK ABOUT and TELL ABOUT */
class DefaultAskTellTopic: DefaultTopic
    includeInList = [&askTopics, &tellTopics]
    matchScore = 4    
;

/* Default Topic to match GIVE and SHOW */
class DefaultGiveShowTopic: DefaultTopic
    includeInList = [&giveTopics, &showTopics]
    matchScore = 4
;

/* Default Topic to match ASK ABOUT */
class DefaultAskTopic: DefaultTopic
    includeInList = [&askTopics]
    matchScore = 5
;

/* Default Topic to match TELL (SOMEONE) ABOUT */
class DefaultTellTopic: DefaultTopic
    includeInList = [&tellTopics]
    matchScore = 5
;

/* Default Topic to match TALK ABOUT */
class DefaultTalkTopic: DefaultTopic
    includeInList = [&talkTopics]
    matchScore = 5
;

/* Default Topic to match GIVE (something to someone) */
class DefaultGiveTopic: DefaultTopic 
    includeInList = [&giveTopics]
    matchScore = 5
;

/* Default Topic to match SHOW (something to someone) */
class DefaultShowTopic: DefaultTopic
    includeInList = [&showTopics]
    matchScore = 5
;

/* Default Topic to match ASK (someone) FOR (something) */
class DefaultAskForTopic: DefaultTopic
    includeInList = [&askForTopics]
    matchScore = 5
;

/* Default Topic to match SAY (something) */
class DefaultSayTopic: DefaultTopic
    includeInList = [&sayTopics]
    matchScore = 5
    
    topicResponse = "<<(getActor.defaultSayResponse)>>"
;


/* Default Topic to match ASK (WHO/WHAT/WHY/WHERE/WHEN/HOW/IF) */
class DefaultQueryTopic: DefaultTopic
    includeInList = [&queryTopics]
    matchScore = 5
;

/* Default Topic to match SAY (something) or ASK (WHO/WHAT/WHY etc.) */
class DefaultSayQueryTopic: DefaultTopic
    includeInList = [&sayTopics, &queryTopics]
    matchScore = 4
;

/* DefaultTopic to match SAY (something) OR TELL (someone) ABOUT (something) */
class DefaultSayTellTopic: DefaultTopic
    includeInList = [&sayTopics, &tellTopics]
    matchScore = 4
;

/* 
 *   DefaultTopic to match TELL (someone) ABOUT (something) OR TALK ABOUT
 *   (something)
 */
class DefaultTellTalkTopic: DefaultTopic
    includeInList = [&tellTopics, &talkTopics]
    matchScore = 4
;

/* 
 *   DefaultTopic to match SAY (something) OR TELL (someone) ABOUT (something)
 *   OR TALK ABOUT (something)
 */
class DefaultSayTellTalkTopic: DefaultTopic
    includeInList = [&sayTopics, &tellTopics, &talkTopics]
    matchScore = 3
;

/* Default Topic to match ASK ABOUT/HOW/WHAT/WHY/WHEN/WHO/IF/WHERE etc */
class DefaultAskQueryTopic: DefaultTopic
    includeInList = [&queryTopics, &askTopics]
    matchScore = 4
;

/* 
 *   DefaultTopic to match orders directed to this actor by the player
 *   (character)
 */
class DefaultCommandTopic: CommandTopicHelper, DefaultTopic
    includeInList = [&commandTopics]
    matchScore = 5
    matchObj = [Action]
;

/* A TopicEntry that matches ASK ABOUT */
class AskTopic: ActorTopicEntry
    includeInList = [&askTopics]
;

/* A TopicEntry that matches TELL ABOUT */
class TellTopic: ActorTopicEntry
    includeInList = [&tellTopics]
;

/* A TopicEntry that matches ASK ABOUT or TELL ABOUT*/
class AskTellTopic: ActorTopicEntry
    includeInList = [&askTopics, &tellTopics]
;

/* A TopicEntry that matches ASK ABOUT or TELL ABOUT or SHOW*/
class AskTellShowTopic: ActorTopicEntry
    includeInList = [&askTopics, &tellTopics, &showTopics]
;

/* A TopicEntry that matches ASK ABOUT or TELL ABOUT or GIVE or SHOW*/
class AskTellGiveShowTopic: ActorTopicEntry
    includeInList = [&askTopics, &tellTopics, &giveTopics, &showTopics]   
;

/* A TopicEntry that matches ASK FOR */
class AskForTopic: ActorTopicEntry
    includeInList = [&askForTopics]
;

/* A TopicEntry that matches ASK ABOUT or ASK FOR*/
class AskAboutForTopic: ActorTopicEntry
    includeInList = [&askForTopics, &askTopics]
;

/* A TopicEntry that matches GIVE TO */
class GiveTopic: ActorTopicEntry
    includeInList = [&giveTopics]
;

/* A TopicEntry that matches SHOW TO */
class ShowTopic: ActorTopicEntry
    includeInList = [&showTopics]
;

/* A TopicEntry that matches GIVE TO or SHOW TO */
class GiveShowTopic: ActorTopicEntry
    includeInList = [&giveTopics, &showTopics]
;

class TellTalkShowTopic: ActorTopicEntry
    includeInList = [&tellTopics, &talkTopics, &showTopics]   
;


/* 
 *   SpecialTopic is the base class for two kinds of TopicEntry that extend the
 *   conversation system beyong basic ask/tell: SayTopic and QueryTopic. The
 *   SpecialTopic class defines the common handling but is not used directly in
 *   game code, which will use either SayTopic or QueryTopic
 */
class SpecialTopic: ActorTopicEntry
    
    /* 
     *   Carry out the initialization (actually preinitialization) of a
     *   SpecialTopic
     */
    initializeTopicEntry()
    {
        /* First carry out the inherited handling */
        inherited;
        
        
        /* 
         *   if the matchPattern contains a semi-colon assume it's not a regex
         *   match pattern but the vocab for a new Topic object.
         */        
        
        if(matchPattern != nil && (matchPattern.find(';') != nil ||                             
                                   matchPattern.find(rex) == nil))            
        {
            /* 
             *   first see if there's already a Topic that has our matchPattern
             *   as its vocab.
             */            
            matchObj = findMatchingTopic(matchPattern);
            
            /* if we found a matching topic, we're done. */
            if(matchObj != nil)
            {
                /* set the matchPattern to nil, since we shan't be using it. */
                matchPattern = nil;               
            }
            else
            {
                
                /* create a new Topic object using the matchPattern as its vocab */
                matchObj = new Topic(matchPattern);
                
                /* then set the matchPattern to nil, since we shan't be using it. */
                matchPattern = nil;
                
                /* add the new matchObj to the universal scope list */
                World.universalScope += matchObj;
            }
        }
        
        /* 
         *   Although the inherited handling might have built our name property
         *   already, it won't have done if we created our matchObj from our
         *   matchPattern property, so if need be we try building it again here.
         */
        if(autoName)
           buildName();
        
        /* 
         *   It may be we want this SpecialTopic also to respond to a
         *   conventional ASK ABOUT X or TELL ABOUT X. We can do this by
         *   defining the askMatchObj and tellMatchObj properties.
         *
         *   First check if our askMatchObj is non-nil and equal to our
         *   tellMatchObj. If so then we want this SpecialTopic also to behave
         *   like an AskTellTopic that matches askMatchObj. To that end we
         *   create a SlaveTopic to represent the AskTellTopic
         */
        if(askMatchObj != nil && askMatchObj == tellMatchObj)
        {
            new SlaveTopic(askMatchObj, self, [&askTopics, &tellTopics]);            
        }
        
        /* 
         *   Otherwise, if we have an askMatchObj, create a SlaveTopic to
         *   represent us as an AskTopic so that we also match ASK ABOUT
         *   askMatchObj.
         */
        else if(askMatchObj != nil)
            new SlaveTopic(askMatchObj, self, [&askTopics]);
        
        /*   
         *   If we have a tellMatchObj that's different from our askMatchObj
         *   (and non-nil), create a new SlaveTopic to represent a TellTopic
         *   that we also match.
         */
        if(tellMatchObj != nil && tellMatchObj != askMatchObj)
            new SlaveTopic(tellMatchObj, self, [&tellTopics]);
    }
    
    /* 
     *   A Regular expression pattern to look for the kinds of characters we'd
     *   expect to find in our matchPattern property if it actually represents a
     *   regular expression for this TopicEntry to match. We use this to help
     *   determine whether the matchPattern property contains a regex to match
     *   our the vocab of a Topic object to create on the fly.
     */    
    rex = static new RexPattern('<langle|rangle|star|dollar|vbar|percent|carat>')
    
    /* 
     *   If we want this SpecialTopic also to match an ASK ABOUT command, define
     *   the askMatchObj to hold the topic or list of topics that said ASK ABOUT
     *   command should match here.
     */
    askMatchObj = nil
    
    /* 
     *   If we want this SpecialTopic also to match an TELL ABOUT command,
     *   define the askMatchObj to hold the topic or list of topics that said
     *   TELL ABOUT command should match here.
     */
    tellMatchObj = nil
    
    /* 
     *   For a SpeciallTopic make constructing a name property automatically the
     *   default.
     */
    autoName = true
    
;

/*  
 *   A SlaveTopic is a special kind of TopicEntry created by a SpecialTopic to
 *   function as an AskTopic, TellTopic or AskTellTopic that produces the same
 *   response at the SpecialTopic. Game code would not normally define
 *   SlaveTopics directly
 */
class SlaveTopic: ActorTopicEntry
    
    /* Construct a SlaveTopic */
    construct(matchObj_, masterObj_, includeInList_)
    {
        /* Note the Topic/Object (or objects) this TopicEntry should match. */           
        matchObj = matchObj_;
        
        /* 
         *   Note our masterObj, which will be the SpecialTopic that called our
         *   c constructor.
         */
        masterObj = masterObj_;
        
        /*  Note which list or lists of TopicEntries we should be included in */
        includeInList = includeInList_;
        
        /*  Our location is the same as our masterObj's location. */
        location = masterObj.location;
        
        /*  Our matchScore is the same as our masterObj's matchScore. */
        matchScore = masterObj.matchScore;
        
        /*  Our scoreBoost is the same as our masterObj's location. */
        scoreBoost = masterObj.scoreBoost;
        
        /* Carry out our initialization as a TopicEntry. */
        initializeTopicEntry();
    }
    
    initializeTopicEntry()
    {
        /* 
         *   Only carry out our initialization if we haven't been initialized
         *   already.
         */
        if(!initialized)
        {
            /* Carry out the inherited handling. */
            inherited();
            
            /* Note that we've now been initialized. */
            initialized = true;
        }
    }
    
    /* 
     *   To handle this topic we simply call the handleTopic method on our
     *   masterObj (i.e. the SpecialTopic that created us)
     */
    handleTopic() { masterObj.handleTopic(); }
    
    /* Our masterObj is the SpecialTopic that created us */
    masterObj = nil
    
    /* Flag: has this SlaveTopic already been initialized. */
    initialized = nil
;

/* 
 *   A QueryTopic is a kind of SpecialTopic that extends the range of questions
 *   that the player (character) can ask an NPC from ASK ABOUT so-and-so to ASK
 *   WHO/WHAT/WHY/WHERE/WHETHER/IF/HOW so-and-so. The type of question to be
 *   matched (who/what/why/when etc.) needs to be defined on a QueryTopic's
 *   qType property (so that it can be matched by the QueryTopic's grammar). The
 *   remainder of the question is the Topic a particular QueryTopic matches.
 */
class QueryTopic: SpecialTopic
    
    /* 
     *   Check whether this QueryTopic matches the question asked. For it to do
     *   so it must match not only the topic but the qType (query type)
     */
    matchTopic(top)
    {
        /* 
         *   A QueryTopic can match more than one query type, so first we split
         *   our qType property into a potential list.
         */
        local qtList = qtype.split('|');
        
        /* 
         *   If the action's qType isn't in our list of qTypes, then we don't
         *   match the question asked, so return nil to indicate a failure to
         *   match.
         */
        if(qtList.indexOf(gAction.qType) == nil)
            return nil;
                
        /* 
         *   Otherwise carry out the inherited handling to see whether we match
         *   the topic (i.e., the rest of the question following the qType word)
         */
        return inherited(top);
    }
    
    /* 
     *   The list of query types we match, e.g. 'where'. To match multiple types
     *   list them divided by a vertical bar, e.g. 'if|whether'
     */
    qtype = nil    
    
    
    initializeTopicEntry()
    {
        /* 
         *   If qtype isn't specified but matchPattern is, take the first word
         *   of the matchPattern to be the qtype.
         */
        if(qtype == nil && matchPattern != nil)
        {
            /* 
             *   Find the first space in our matchPattern string. This should
             *   mark the end of the first word.
             */
            local idx = matchPattern.find(' ');
            
            /* Only do anything if we find a space */
            if(idx)
            {
                /* 
                 *   Take the qType to be the first word, i.e. the beginning of
                 *   the string up to but not including the first space
                 */
                qtype = matchPattern.substr(1, idx - 1);
                
                /*   
                 *   Take the true matchPattern -- or rather the Topic vocab --
                 *   to be the rest of the matchPattern string following the
                 *   first space.
                 */
                matchPattern = matchPattern.substr(idx + 1).trim();
            }
            
        }
     
        /* Carry out the inherited handling. */
        inherited;    
    }
    
    /* 
     *   When we build the name of a QueryTopic (for use in a list of topic
     *   suggestions) we need to include the query type (qType).
     */
    buildName()
    {
        /* 
         *   Don't attempt to construct the name if we already have one or we
         *   don't have a matchObj.
         */
        if(name == nil && matchObj != nil)
        {
            /* 
             *   Split our qType property into a list of query types, since it
             *   could be specified as several possible types separated by
             *   vertical bars
             */
            local qList = qtype.split('|');
            
            /*   
             *   Prepend the first word from the query type list to the name of
             *   our matchObj to create our name
             */
            name = qList[1] + ' ' + valToList(matchObj)[1].name; 
        }
    }
    
    /* A QueryTopic belongs in the queryTopics list of its TopicDatabase */
    includeInList = [&queryTopics]          
;


/* 
 *   A SayTopic is a kind of SpecialTopic that allows the player (character) to
 *   say virtually anything (within reason) to an NPC; a SayTopic may be
 *   triggered by a command that explicitly begins with SAY, but it may also be
 *   triggered by any combination of words that matches its matchObj and doesn't
 *   correspond to any other recognizable command. This allows the player to
 *   respond, for example, with either SAY YOU DON'T KNOW or just I DON'T KNOW,
 *   to trigger an appropriately defined SayTopic.
 */
class SayTopic: SpecialTopic
   
    /* 
     *   When we construct the name of a SayTopic we use the name property of
     *   its matchObj rather that theName property, since it won't normally make
     *   sense to include the definite article at the beginning of suggestions
     *   of things that can be said.
     */
    buildName()
    {
        /* 
         *   We don't try to construct a name if we have one already or if we
         *   don't have a matchObhj.
         */
        if(name == nil && matchObj != nil)
            /* 
             *   The matchObj property could in principle be specified as a
             *   list, in which case use the first (and possibly only) item in
             *   the list to construct our name.
             */
            name = valToList(matchObj)[1].name; 
    }

    /* SayTopics belong in the sayTopics list of their TopicDatabase */    
    includeInList = [&sayTopics]
    
    /* 
     *   When a SayTopic is suggested we normally precede its name by 'say',
     *   e.g. 'say you are happy'. In some cases an author might want to use a
     *   SayTopic to match input that's better without the initial 'say', e.g.
     *   'tell a lie', in which case set includeSayInName to nil to suppress the
     *   initial 'say' in topic inventory listings.
     */
    
    includeSayInName = true
;

/* A TalkTopic is a TopicEntry that responds to TALK ABOUT so-and-so. */
class TalkTopic: ActorTopicEntry
    includeInList = [&talkTopics]
;
    
/* 
 *   A TellTalkTopic is a TopicEntry that responds to TELL ABOUT or TALK ABOUT
 *   so-and-so.
 */
class TellTalkTopic: TalkTopic
    includeInList = [&tellTopics, &talkTopics]
;
 
/* 
 *   An AskTellTalkTopic is a TopicEntry that responds to ASK ABOUT or TELL
 *   ABOUT or TALK ABOUT so-and-so.
 */
class AskTellTalkTopic: TalkTopic
    includeInList = [&askTopics, &tellTopics, &talkTopics]
;

/* 
 *   An AskTalkTopic is a TopicEntry that responds to ASK ABOUT or TALK ABOUT
 *   so-and-so.
 */
class AskTalkTopic: TalkTopic
    includeInList = [&askTopics, &talkTopics]
;


/* 
 *   An initiateTopic is used for conversational topics initiated by the actor
 *   through a call to initiateTopic() on the actor or ActorState
 */
class InitiateTopic: ActorTopicEntry
    includeInList = [&initiateTopics]
;

/*  
 *   An AltTopic is a TopicEntry that can be located inside another TopicEntry;
 *   to provide an alternative response to the same conversational commands when
 *   its isActive property is true. An AltTopic takes precedence over its
 *   enclosing TopicEntry when its active property is true, but otherwise takes
 *   most of its properties (apart from its topicResponse) from the TopicEntry
 *   in which it is located. You can have as many AltTopics as you like
 *   associated with any given TopicEntry; the one that will be used will be the
 *   last to be defined for which isActive is true.
 */
class AltTopic: ActorTopicEntry
    
    /* 
     *   By default we take most of our property values from the corresponding
     *   property on the TopicEntry we're located in
     */
    getActor = location.getActor
      
    convKeys = location.convKeys    
    includeInList = location.includeInList
    name = location.name
    suggestAs = location.suggestAs 
 
    allowAction = location.allowAction
    myAction = location.myAction
    actionPhrase = (location.actionPhrase)
    
    /* 
     *   We match if our location matches, but if it does match we add our
     *   relative source text order to our location's match score to give us a
     *   higher score the later we're defined after our associated TopicEntry.
     */
    matchTopic(top)
    {
        /* Note the topic we're trying to match. */
        topicMatched = top;
        
        local score = location.matchTopic(top);
        
        return score == nil ? nil : score + sourceTextOrder -
            location.sourceTextOrder;
    }
    

;


/* 
 *   A NodeContinuationTopic is aspecial kind of InitiateTopic that can be used
 *   to prompt the player/pc when particular convKeys have been activated. It is
 *   generally used when a Conversation Node is active to remind the player that
 *   the player character's conversation partner is waiting for an answer.
 */

class NodeContinuationTopic: InitiateTopic
    matchObj = nodeObj
    
    /* 
     *   We're only active when one or more of our keys is active (having been
     *   activated through an <.convnode> tag).
     */
    active = (nodeActive && inherited)
    
    /* 
     *   Particular instances must override this property to stipulate which
     *   keys we're active for. (This isn't needed if the NodeContinuationTopic
     *   is located in a ConvNode, since the ConvNode will then take care of
     *   this for us).
     */
    convKeys = nil
    
    handleTopic()
    {
        /* 
         *   We don't want a NodeContinuationTopic to reset the active keys, so
         *   we send a convstay tag to retain them.
         */
        "<.p><.convstay>";
        
        /* Carry out the inherited handling. */
        inherited();
    }
;

/* 
 *   A NodeEndCheck may optionally be assigned to a Conversation Node (as
 *   defined on the convKeys property, or through being located in a ConvNode
 *   object) to decide whether a conversation is allowed to end while it's at
 *   this node. There's no need to define one of these objects for a
 *   conversation node if you're happy for the conversation to be ended during
 *   it under all circumstances.
 */

class NodeEndCheck: EndConvBlocker, InitiateTopic
    matchObj = nodeEndCheckObj
    
    /* 
     *   We're only active when one or more of our keys is active (having been
     *   activated through an <.convnode> tag).
     */
    active = (nodeActive && inherited)
    
    
    /* 
     *   Particular instances must override this property to stipulate which
     *   keys we're active for, unless this NodeEndCheck is located within a
     *   ConvNode object which will take care of this for us. Note that instead
     *   of locating a NodeEndCheck in a particular ConvNode, you can specify
     *   the convKeys for a number of ConvNodes here, and this NodeEndCheck will
     *   then apply to them all.
     */
    convKeys = nil
    
    /*   
     *   Decide whether the conversation can be ended for reason while the
     *   conversation is at this node. By default we simply return true but
     *   instances should override to return nil when the conversation should
     *   not be permitted to end. When the method returns nil it should also
     *   display a message saying why the conversation may not be ended.
     */    
    canEndConversation(reason)
    {
        return true;
    }
    
    /* 
     *   Do nothing here; this class only exists for the sake of its
     *   canEndConversation() method.
     */    
    handleTopic() { }
    
    
    
;

/* 
 *   EndConvBlocker is a mix-in class whose sole function is to define the
 *   blockEndConv method common to Actor, ActorState and NodeEndCheck
 */
EndConvBlocker: object
    /* 
     *   Convenience method that notes that conversation has occurred on this
     *   turn and returns nil. This is to allow us to use:
     *.
     *   return blockEndConv;
     *
     *   in the canEndConversation method to suppress the output of any
     *   NodeContinuationTopic on this turn.     */
    
    blockEndConv()
    {
        /* Note that the actor has conversed on this turn */
        getActor.noteConversed();
        
        /* 
         *   Return nil to signal that we're not allowing the conversation to
         *   end.
         */
        return nil;
    }
;

/* Singleton object to allow initiateTopic to trigger a NodeContinuationTopic */
nodeObj: object;

/* Singleton object to allow initiateTopic to trigger a NodeEndCheck */
nodeEndCheckObj: object;


/* 
 *   Preinitialize all the Actors in the game and the objects associated with
 *   them.
 */
actorPreinit:PreinitObject
    execute()
    {       
        /* Initialize every ActorState defined in the game */
        forEachInstance(ActorState, {a: a.initializeActorState() } );
        
        /* Initialize every ActorTopicEntry definined in the game */
        forEachInstance(ActorTopicEntry, {a: a.initializeTopicEntry() });
        
        /* 
         *   Set up a new Schedulable in the game to run our eachTurn method each
         *   turn
         */
                    
        eventManager.schedulableList += actorSchedule;
            
//            new Daemon(self, &eachTurn, 1);
//        
//        /*   Give the actorDaemon a relatively late running order */
//        actorDaemon.eventOrder = 300;
    }
    
    /* 
     *   Make use that various other preinitializations presupposed by our own
     *   have been carried out before ours
     */
    execBeforeMe = [World, libObjectInitializer, pronounPreinit]
   
;

actorSchedule: Event
    eventOrder = 100
    executeEvent() { forEachInstance(Actor, {a: a.takeTurn() }); }
;


/* ------------------------------------------------------------------------ */
/*
 *   Conversation manager output filter.  We look for special tags in the output
 *   stream:
 *
 *   <.reveal key> - add 'key' to the knowledge token lookup table.  The 'key'
 *   is an arbitrary string, which we can look up in the table to determine if
 *   the key has even been revealed.  This can be used to make a response
 *   conditional on another response having been displayed, because the key will
 *   only be added to the table when the text containing the <.reveal key>
 *   sequence is displayed.
 *
 *   <.unreveal key> - remove 'key from the knowledge token lookup table.
 *
 *   <.inform key> - add 'key' to our actor's knowledge token lookup take. The
 *   'key' is an arbitrary string, which we can look up in the table to
 *   determine if the actor has ever been informed about this key.  This can be
 *   used to make a response conditional on another response having been
 *   displayed, because the key will only be added to the information table when
 *   the text containing the <.inform key> sequence is displayed.
 *
 *   <.convnode name> - add 'name' to the current list of convKeys (this
 *   actually adds it to the actor's pendingKeys for use on the next turn); this
 *   is normally used to trigger a Conversation Node that's defined to match the
 *   same name.
 *
 *   <.convodet name> does the same as <.convnode name> and additionally
 *   schedules a topic inventory (a listing of suggested topics); this can be
 *   used to ensure that the player knows what conversational options are
 *   available in the node we're about to enter, where this isn't obvious from
 *   the context.
 *
 *   <.convstay> - retain the same list of active keys for the next
 *   conversational response (and thus has the effect of making the conversation
 *   remain in the same conversation node).
 *
 *   <.convstayt> - does the same as <.convstay> but additionally schedules a
 *   topic inventory.
 *
 *   <.topics> - schedule a topic inventory for the end of the turn (just before
 *   the next command prompt)
 *
 *   <.arouse key> Set the curiosityAroused property to true for all
 *   TopicEntries whose convKeys include key
 *
 *   <.suggest key> Schedule a topic inventory for all topic entries whose
 *   convKeys include key.
 *
 *   <.sugkey key> Set our actor's suggestionKey to key (this potentially
 *   restricts the list of topics that will be suggested)
 *
 *   <.activate key> Set the activated property to true for every topic entry
 *   whose convKeys list includes key.
 *
 *   <.agenda item> Add item to the agenda list of our Actor and any associated
 *   DefaultAgendaTopics.
 *
 *   <.remove item> Remove item from the agenda list of our Actor and any
 *   associated DefaultAgendaTopics.
 *
 *   <.state newstate> Change our actor's current ActorState to newstate.
 *
 *   <.known obj> Mark obj (a Thing or Topic) as now being known (i.e. familiar)
 */
conversationManager: OutputFilter, PreinitObject
    /*
     *   Custom extended tags.  Games and library extensions can add their
     *   own tag processing as needed, by using 'modify' to extend this
     *   object.  There are two things you have to do to add your own tags:
     *   
     *   First, add a 'customTags' property that defines a regular
     *   expression for your added tags.  This will be incorporated into
     *   the main pattern we use to look for tags.  Simply specify a
     *   string that lists your tags separated by "|" characters, like
     *   this:
     *   
     *   customTags = 'foo|bar'
     *   
     *   Second, define a doCustomTag() method to process the tags.  The
     *   filter routine will call your doCustomTag() method whenever it
     *   finds one of your custom tags in the output stream.  
     */
    customTags = nil
    doCustomTag(tag, arg) { /* do nothing by default */ }

    /* 
     *   The actor we're dealing with is the player character's current
     *   interlocutor
     */
    respondingActor = (gPlayerChar.currentInterlocutor)
    
    /* filter text written to the output stream */
    filterText(ostr, txt)
    {
        local start;
        
        /* scan for our special tags */
        for (start = 1 ; ; )
        {
            local match;
            local arg;
            local tag;
            local nxtOfs;
            local obj;
            
            /* scan for the next tag */
            match = rexSearch(tagPat, txt, start);

            /* if we didn't find it, we're done */
            if (match == nil)
                break;

            /* note the next offset */
            nxtOfs = match[1] + match[2];

            /* get the argument (the third group from the match) */
            arg = rexGroup(3);
            if (arg != nil)
                arg = arg[3];

            /* pick out the tag */
            tag = rexGroup(1)[3].toLower();

                       
            /* check which tag we have */
            switch (tag)
            {
                /* 
                 *   We distiguish between information that is revealed *to* the
                 *   player char (by using a <.reveal> tag) and information
                 *   imparted by the player char to other characters (using the
                 *   <.inform> tag). Game authors do not have to observe this
                 *   distinction if they only want to use <.reveal> as in the
                 *   adv3 library, but it may be a useful distinction for some
                 *   games.
                 *
                 *   Note that there is one global table of revealed tags (on
                 *   libGlobal) use for revealed items, but that each actor
                 *   (apart from the player char) maintains its own table of
                 *   items imparted through inform tags.
                 *
                 *   Note also that a <.reveal> or <.inform> tag causes the tag
                 *   to be added to the informNameTag table of every actor who
                 *   can hear the conversation, not just the current
                 *   interlocutor.
                 *
                 *   If <.inform> tags are used as well as <.reveal> tags it's
                 *   therefore a good idea to regard the tags as a global
                 *   namespace -- i.e. one tag value should be used consistently
                 *   to represent one piece of information.
                 *
                 *   Finally, note that the gRevealed() macro only adds the tag
                 *   to the libGlobal table - it doesn't result in any other
                 *   actors being informed. It can therefore be used (among
                 *   other things) to reveal information that remains private to
                 *   the player char.
                 */
             
                
            case 'reveal':
                /* reveal the key by adding it to our database */
                setRevealed(arg);
                break;
                
                /* unreveal the key by removing it from our database */
            case 'unreveal':
                setUnrevealed(arg);
                break;
                
            case 'inform':    
                /* reveal the key by adding it to the actor's database */
                setInformed(arg);
                break;

         
            case 'convnode':
            case 'convnodet':    
                /* 
                 *   if there's a current responding actor, add the key to its
                 *   list of pending keys (for use on the next conversational
                 *   turn).
                 */                
                if(respondingActor != nil)
                {
                    /* 
                     *   If we are processing several convnode tags on the same
                     *   turn, we want them all to take effect; otherwise we
                     *   want the new convnode to replace any that was
                     *   previously in effect.
                     */
                    if(convnodeSetTurn == libGlobal.totalTurns)
                        respondingActor.addPendingKey(arg);
                    else
                        respondingActor.pendingKeys = [arg];
                }
                
                /* Note that we have set a new convnode on this turn */
                convnodeSetTurn = libGlobal.totalTurns;
                
                /* 
                 *   We deliberatelty don't put a BREAK; statement here since we
                 *   need to fall through ro the convstay behaviour to ensure that
                 *   our keys aren't obliterated as soon as they're set.
                 */                
            case 'convstay':
            case 'convstayt':    
                /* 
                 *   Leave the responding actor in the old conversation
                 *   node - we don't need to change the ConvNode, but we do
                 *   need to note that we've explicitly set it 
                 */
                if (respondingActor != nil)
                    respondingActor.keepPendingKeys = true;
               
                /* 
                 *   If the tag was 'convnode' we didn't ask for a topic
                 *   inventory, so we need to avoid falling through. If the tag
                 *   was 'convnodet' or 'convstayt' we want a topic inventory
                 *   too, so we fall through to the 'topics' tag.
                 */                
                if(tag is in ('convnodet', 'convstayt') 
                   && respondingActor != nil)                    
                    scheduleTopicInventory(respondingActor.pendingKeys);
                    
                 break;

            case 'topics':
                /* schedule a topic inventory listing */
                if (respondingActor != nil)
                    scheduleTopicInventory(respondingActor.pendingKeys == [] ? 
                                           respondingActor.suggestionKey
                                           : respondingActor.pendingKeys);
                break;
                
            case 'arouse':
                /* 
                 *   make the curiosityAroused property true for Topic Entries
                 *   with the appropriate key.
                 */
                if (respondingActor != nil)
                    respondingActor.arouse(arg);
                
                break;
                
            case 'suggest':
                 /* translate the string 'nil' to an actual nil */
                if(arg == 'nil')
                    arg = nil;
                
                /* 
                 *   If we have a responding actor, schedule a topic inventory
                 *   for topic entries that match arg
                 */
                if (respondingActor != nil)
                    scheduleTopicInventory(arg);
                break;
                
            case 'sugkey':
                /* translate the string 'nil' to an actual nil */
                if(arg == 'nil')
                    arg = nil;
                /* set the suggestionKey on the responding actor to arg */
                if (respondingActor != nil)
                    respondingActor.suggestionKey = arg;
                break;    
                
            case 'activate':
                /* 
                 *   Set the activated property to true for all Topic Entries
                 *   with the appropriate key.
                 */
                if (respondingActor != nil)
                    respondingActor.makeActivated(arg);
                break;
                
            case 'deactivate':
                /* 
                 *   Set the activated property to true for all Topic Entries
                 *   with the appropriate key.
                 */
                if (respondingActor != nil)
                    respondingActor.makeDeactivated(arg);
                break;
                
            case 'agenda':
                /* add an agenda item to all relevant objects */
                
                /* 
                 *   Obtain the object corresponding to arg (a string value)
                 *   from our object name table
                 */
                obj = objNameTab[arg];
                
                /* 
                 *   If the object we're trying to add to an agenda list isn't
                 *   an AgendaItem belonging to our respondingActor, display an
                 *   error message.
                 */
                if(obj == nil || !obj.ofKind(AgendaItem) || 
                   obj.getActor != respondingActor)
                {
                    showAgendaError(tag, arg);
                }
                else
                    /* 
                     *   Otherwise add the AgendaItem obj to our
                     *   respondingActor's agendaList and that of any associated
                     *   DefaultAgendaTopics
                     */
                    respondingActor.addToAllAgendas(obj);
                break;
                
            case 'remove':
                /* remove an agenda item from all relevant objects */
                
                /* 
                 *   Obtain the object corresponding to arg (a string value)
                 *   from our object name table
                 */
                obj = objNameTab[arg];
                
                /* 
                 *   If the object we're trying to add to an agenda list isn't
                 *   an AgendaItem belonging to our respondingActor, display an
                 *   error message.
                 */
                if(obj == nil || !obj.ofKind(AgendaItem) || 
                   obj.getActor != respondingActor)
                {
                    showAgendaError(tag, arg);
                }
                else
                    /* 
                     *   Otherwise remove the AgendaItem obj from our
                     *   respondingActor's agendaList and that of any associated
                     *   DefaultAgendaTopics
                     */
                    respondingActor.removeFromAllAgendas(obj);
                break;
                
            case 'state':
                /* change ActorState */
                
                /* 
                 *   Obtain the object corresponding to arg (a string value)
                 *   from our object name table
                 */
                obj = objNameTab[arg];
                
                /* Convert a string 'nil' to an actual nil */
                if(arg == 'nil')
                    obj = nil;                
                
                /* 
                 *   Otherwise if the object we're trying to add to an agenda
                 *   list isn't an ActorState belonging to our respondingActor,
                 *   display an error message.
                 */
                else if(obj == nil || !obj.ofKind(ActorState) || 
                   obj.getActor != respondingActor)
                {
                    showStateError(tag, arg);
                }
                else
                    /* 
                     *   Otherwise set our respondingActor's ActorState to the
                     *   new state requested (obj).
                     */
                    respondingActor.setState(obj);
                break;
            
            case 'known':
                /* mark as item as known. */
                
                /* 
                 *   Obtain the object corresponding to arg (a string value)
                 *   from our object name table
                 */
                obj = objNameTab[arg];
                             
                /* 
                 *   If the obj doesn't exist, or it isn't a Mentionable,
                 *   display an error message
                 */
                if(obj == nil || !obj.ofKind(Mentionable))
                {
                    showKnownError(tag, arg);
                }
                else
                    /* 
                     *   Otherwise mark obj as known about by the player
                     *   character
                     */
                   gPlayerChar.setKnowsAbout(obj);
                break;
                
            default:
                /* check for an extended tag */
                doCustomTag(tag, arg);
                break;
            }

            /* continue the search after this match */
            start = nxtOfs;
        }

        /* 
         *   remove the tags from the text by replacing every occurrence
         *   with an empty string, and return the result 
         */
        return rexReplace(tagPat, txt, '', ReplaceAll);
    }

    
    /* The turn on which we last processed a convnode tag */
    convnodeSetTurn = 0
    
    /* regular expression pattern for our tags */
    tagPat = static new RexPattern(
        '<nocase><langle><dot>'
        + '(reveal|unreveal|agenda|remove|state|known|activate|inform|convstay|topics'
        + (customTags != nil ? '|' + customTags : '')
        + '|arouse|suggest|sugkey|convnode|convnodet|convstayt|deactivate)'
        + '(<space>+(<^rangle>+))?'
        + '<rangle>')

	/* Provided we have a respondingActor, call its manageKeys method. */	
    manageKeys()
    {
	   if(respondingActor != nil)
	      respondingActor.manageKeys();
    }	
		
    /*
     *   Schedule a topic inventory request.  Game code can call this at
     *   any time to request that the player character's topic inventory
     *   be shown automatically just before the next command prompt.  In
     *   most cases, game code won't call this directly, but will request
     *   the same effect using the <.topics> tag in topic response text.  
     */
    scheduleTopicInventory(key = nil)
    {
        /* note that we have a request for a prompt-time topic inventory */
        pendingTopicInventory = true;      
        
        /* note the key to be used for this request. */
        pendingTopicInventoryKey = key;
    }

    /*
     *   Mark a tag as revealed.  This adds an entry for the tag to the
     *   revealedNameTab table.  We simply set the table entry to 'true'; the
     *   presence of the tag in the table constitutes the indication that the
     *   tag has been revealed.
     *
     *   (Games and library extensions can use 'modify' to override this and
     *   store more information in the table entry.  For example, you could
     *   store the time when the information was first revealed, or the location
     *   where it was learned.  If you do override this, just be sure to set the
     *   revealedNameTab entry for the tag to a non-nil and non-zero value, so
     *   that any code testing the presence of the table entry will see that the
     *   slot is indeed set.)
     *
     *   The actual method and the revealedNameTab are on libGlobal rather than
     *   here in order to make them available to games that don't include
     *   actor.t.
     */
    setRevealed(tag)
    {
        /* Note that our tag has been revealed */
        libGlobal.setRevealed(tag);
        
        /* 
         *   If something has just been revealed to us, it has also just been
         *   revealed to every other actor in the vicinity who could overhear
         *   the conversation (including the actor who has just spoken, if there
         *   is one; if there isn't then the revealed tag is presumably being
         *   used for a non-conversational purpose, so we don't try to inform
         *   any other actors).
         *
         *   Note that we only do this if our respondingActor wants to allow it
         *   through its informOverheard property. If we want to model a private
         *   conversation that other people present don't pick up, we can
         *   override informOverheard on the the current ActorState or
         *   actorInformedOverhead on the Actor.
         */                
        if(respondingActor != nil && respondingActor.informOverheard )
        {
            
            forEachInstance(Actor, new function(a) {
                if(a != gPlayerChar && Q.canHear(a, respondingActor))
                    a.setInformed(tag);
            } );
            
        }
    }
    
    /*
     *   Mark a tag as unrevealed.  This removes an entry for the tag to the revealedNameTab table.
     *
     *   The actual method and the revealedNameTab are on libGlobal rather than here in order to
     *   make them available to games that don't include actor.t.
     */
    setUnrevealed(tag)
    {
        libGlobal.setUnrevealed(tag);       
    }

    /* 
     *   Notify every actor who's in a position to hear that we've just imparted
     *   some information.
     */
    setInformed(tag)
    {
        /*
         *   Note that we only do this if our respondingActor wants to allow it
         *   through its informOverheard property. If we want to model a private
         *   conversation that other people present don't pick up, we can
         *   override informOverheard on the the current ActorState or
         *   actorInformedOverhead on the Actor.
         */
        if(respondingActor.informOverheard)
        {
            forEachInstance(Actor, new function(a) {
                if(a != gPlayerChar && Q.canHear(a, gPlayerChar))
                    a.setInformed(tag);
            } );
        }
        
        /* 
         *   If this is a private conversation (informOverheard = nil), just set
         *   the informed tage on our respondingActor alone.
         */
        else
        {
            respondingActor.setInformed(tag);
        }
        
    }
    
    /* 
     *   Display an error message if the game code tries to add or remove agenda
     *   items from an agendaList using a <.agenda item> or <.remove item> tag,
     *   when item doesn't correspond to a valid AgendaItem, but only do so if
     *   the game has been compiled for debugging.
     */
    showAgendaError(tag, arg)
    {
#ifdef __DEBUG
        "WARNING!!! ";
        if(obj == nil)
            showObjNotExistError(tag, arg, 'Agenda Item');
        else if(!obj.ofKind(AgendaItem))
            showWrongKindofObjectError(tag, arg, 'an Agenda Item');
        else if(obj.getActor != respondingActor)
            showObjDoesNotBelongToActorError(tag, arg, 'Agenda Item');
        
        
#endif        
    }
    
    
    /* 
     *   Display an error message if the game code tries to change our actor's
     *   ActorState via a <.state newstate> tag, when tag doesn't correspond to
     *   a valid ActorState, but only do so if the game has been compiled for
     *   debugging.
     */
    showStateError(tag, arg)
    {
        #ifdef __DEBUG
        "WARNING!!! ";
        if(obj == nil)
            showObjNotExistError(tag, arg, 'Actor State');
        else if(!obj.ofKind(ActorState))
            showWrongKindofObjectError(tag, arg, 'an ActorState');
        else if(obj.getActor != respondingActor)
            showObjDoesNotBelongToActorError(tag, arg, 'Actor State');
        
        #endif
    }

    /* 
     *   Display an error message if the game code tries mark an object as known
     *   about using a <.known obj> tag, when obj doesn't correspond to a valid
     *   Mentionable object, but only do so if the game has been compiled for
     *   debugging.
     */
    showKnownError(tag, arg)
    {
         #ifdef __DEBUG
        "WARNING!!! ";
        if(obj == nil)
            showObjNotExistError(tag, arg, 'Mentionable');
        else if(!obj.ofKind(Mentionable))
            showWrongKindofObjectError(tag, arg, 'a Mentionable');
        
#endif
    }
    
    /* 
     *   Various supporting routines for displaying error messages that are only
     *   needed if the game has been compiled for debugging
     */
    
 #ifdef __DEBUG   
    /* The object referred to by tag doesn't exist */
    showObjNotExistError(tag, arg, typ)
    {
        "<<typ>> <<arg>> for (actor = <<respondingActor.name>> was not added to
        conversationManager.objNameTab or does not exist. Check that you have
        spelled the <<typ>> <<arg>> name correctly. ";
    }
    
    /* The object referred to by tag is the wrong sort of object */
    showWrongKindofObjectError(tag, arg, typ)
    {
        "<tag> is not <<typ>> so can't be used in a <<tag>> tag (see
        TopicEntries for <<respondingActor.theName>> ";
    }
    
    /* The object referred to by tag doesn't belong to the actor in question */
    showObjDoesNotBelongToActorError(tag, arg, typ)
    {
        "<<typ>> <<tag>> does not belong to
            <<respondingActor.theName>>, so can't be used in a <<tag>> tag. ";
    }
 #endif   
    
    /* a vector of actors, indexed by their convMgrID values */
    idToActor = static new Vector(32)

    /* preinitialize */
    execute()
    {
        
        /*  Add ourselves to the list of output filters. */        
        mainOutputStream.addOutputFilter(self);
        
        /* 
         *   Set up the prompt daemon that makes automatic topic inventory
         *   suggestions when appropriate 
         */
        new PromptDaemon(self, &topicInventoryDaemon);
    }

    /*
     *   Prompt daemon: show topic inventory when appropriate.  When a
     *   response explicitly asks us to show a topic inventory using the
     *   <.topics> tag, or when other game code asks us to show topic
     *   inventory by calling scheduleTopicInventory(), we'll show the
     *   inventory just before the command input prompt.  
     */
    topicInventoryDaemon()
    {
        /* if we have a topic inventory scheduled, show it now */
        if (pendingTopicInventory)
        {
            /* 
             *   Show the player character's topic inventory.  This is not
             *   an explicit inventory request, since the player didn't ask
             *   for it.  
             */
            
            if(gPlayerChar.currentInterlocutor != nil)
                gPlayerChar.currentInterlocutor.showSuggestions(nil,
                    pendingTopicInventoryKey); 

            /* we no longer have a pending inventory request */
            pendingTopicInventory = nil;
            pendingTopicInventoryKey = nil;
        }
    }

    /* flag: we have a pending prompt-time topic inventory request */
    pendingTopicInventory = nil
    
    /* The key to use for the pending prompt-time inventory request */
    pendingTopicInventoryKey = nil
    
    objNameTab = static new LookupTable
;

/* 
 *   Base class for items (Actors and DefaultAgendaTopics) that can handle
 *   AgendaItems */

class AgendaManager: object
    
    /* 
     *   Our agendaList is the list of AgendaItems we're ready to execute when
     *   they're isReady property is true.
     */
    agendaList = nil
    
    /* 
     *   add an agenda item. We try to make this as author-proof as possible so
     *   that the method will accept addToAgenda(item), addToAgenda(item1,
     *   item2, ...) or addToAgenda([item1, item2,..])
     */     
    addToAgenda([lst])
    {
        /* if we don't have an agenda list yet, create one */
        if (agendaList == nil)
            agendaList = new Vector(10);
        
        /* add the item or items. */
        foreach(local val in lst)
        {    
            foreach(local cur in valToList(val))
            {
                agendaList += cur;
                
                /* reset the agenda item */
                cur.resetItem();
                
                /* 
                 *   carry out any registration needed - primarily needed for
                 *   FollowAgendaItems.
                 */
                cur.registerItem();
            }
        }
        
        /* 
         *   Keep the list in ascending order of agendaOrder values - this will
         *   ensure that we'll always choose the earliest item that's ready to
         *   run
         */
        agendaList.sort(SortAsc, {a, b: a.agendaOrder - b.agendaOrder});       
    }

    /* remove one or more agenda items */
    removeFromAgenda([lst])
    {
        /* if we have an agenda list, remove the item */
        if (agendaList != nil)
        {
            foreach(local val in lst)
            {
                foreach(local item in valToList(val))
                {
                    agendaList.removeElement(item);                    
                }
            }
        }
    }

    /*
     *   Execute the next item in our agenda, if there are any items in the
     *   agenda that are ready to execute.  We'll return true if we found
     *   an item to execute, nil if not.  
     */
    executeAgenda()
    {
        local item;
        local actor = getActor;

        /* if we don't have an agenda, there are obviously no items */
        if (agendaList == nil)
            return nil;
        
        /* remove any items that are marked as done */
        while ((item = agendaList.lastValWhich({x: x.isDone})) != nil)
        {    
            actor.removeFromAllAgendas(item);
            actor.removeFromPendingAgenda(item);
        }

        /* 
         *   Scan for an item that's ready to execute.  Since we keep the
         *   list sorted in ascending order of agendaOrder values, we can
         *   just pick the earliest item in the list that's ready to run,
         *   since that will be the ready-to-run item with the lowest
         *   agendaOrder number. 
         */
        item = agendaList.valWhich({x: x.isReady});

        /* if we found an item, execute it */
        if (item != nil)
        {
            try
            {
                /* execute the item */
                item.invokeItemBase(self);
                
                /* 
                 *   if the item is done, remove it from all relevant agenda
                 *   lists
                 */
                if(item.isDone)
                    getActor.removeFromAllAgendas(item);
            }
            catch (RuntimeError err)
            {
                /* 
                 *   If an error occurs while executing the item, mark the
                 *   item as done.  This will ensure that we won't get
                 *   stuck in a loop trying to execute the same item over
                 *   and over, which will probably just run into the same
                 *   error on each attempt.  
                 */
                item.isDone = true;

                /* re-throw the exception */
                throw err;
            }

            /* tell the caller we found an item to execute */
            return true;
        }
        else
        {
            /* tell the caller we found no agenda item */
            return nil;
        }
    }    
;



/* ------------------------------------------------------------------------ */
/*
 *   An "agenda item."  Each actor can have its own "agenda," which is a
 *   list of these items.  Each item represents an action that the actor
 *   wants to perform - this is usually a goal the actor wants to achieve,
 *   or a conversational topic the actor wants to pursue.
 *   
 *   On any given turn, an actor can carry out only one agenda item.
 *   
 *   Agenda items are a convenient way of controlling complex behavior.
 *   Each agenda item defines its own condition for when the actor can
 *   pursue the item, and each item defines what the actor does when
 *   pursuing the item.  Agenda items can improve the code structure for an
 *   NPC's behavior, since they nicely isolate a single background action
 *   and group it with the conditions that trigger it.  But the main
 *   benefit of agenda items is the one-per-turn pacing - by executing at
 *   most one agenda item per turn, we ensure that the NPC will carry out
 *   its self-initiated actions at a measured pace, rather than as a jumble
 *   of random actions on a single turn.
 *   
 *   Note that NPC-initiated conversation messages override agendas.  If an
 *   actor has an active ConvNode, AND the ConvNode displays a
 *   "continuation message" on a given turn, then the actor will not pursue
 *   its agenda on that turn.  In this way, ConvNode continuation messages
 *   act rather like high-priority agenda items.  
 */
class AgendaItem: object
    /* 
     *   My actor - agenda items should be nested within the actor using
     *   '+' so that we can find our actor.  Note that this doesn't add the
     *   item to the actor's agenda - that has to be done explicitly with
     *   actor.addToAgenda().  
     */
    getActor() { return location.getActor; }

    /*
     *   Is this item active at the start of the game?  Override this to
     *   true to make the item initially active; we'll add it to the
     *   actor's agenda during the game's initialization.  
     */
    initiallyActive = nil

    /* 
     *   Is this item ready to execute?  The actor will only execute an
     *   agenda item when this condition is met.  By default, we're ready
     *   to execute.  Items can override this to provide a declarative
     *   condition of readiness if desired.  
     */
    isReady = true

    /*
     *   Is this item done?  On each turn, we'll remove any items marked as
     *   done from the actor's agenda list.  We remove items marked as done
     *   before executing any items, so done-ness overrides readiness; in
     *   other words, if an item is both 'done' and 'ready', it'll simply
     *   be removed from the list and will not be executed.
     *   
     *   By default, we simply return nil.  Items can override this to
     *   provide a declarative condition of done-ness, or they can simply
     *   set the property to true when they finish their work.  For
     *   example, an item that only needs to execute once can simply set
     *   isDone to true in its invokeItem() method; an item that's to be
     *   repeated until some success condition obtains can override isDone
     *   to return the success condition.  
     */
    isDone = nil

    /*
     *   The ordering of the item relative to other agenda items.  When we
     *   choose an agenda item to execute, we always choose the lowest
     *   numbered item that's ready to run.  You can leave this with the
     *   default value if you don't care about the order.  
     */
    agendaOrder = 100

    /*
     *   The caller is passed as a parameter so we can tell whether we're being
     *   called from an Actor or from a DefaultAgendaTopic, which may affect
     *   what we want to do -- for example the wording of what the actor says at
     *   this point.
     */
    invokeItemBase(caller)
    {
        /* Note what object we were called from */
        calledBy = caller;
        
        /* Then carry out our invokeItem() method */
        invokeItem();
    }
    
    /* 
     *   invokeItem can test the invokedByActor property to decide whether what
     *   the actor says should be a conversational gambit started on the actor's
     *   own initiative or as a (default) response to something the pc has just
     *   tried to say.     */
    
    invokedByActor = (calledBy == getActor)
    
    /* The object from whose agendaList this AgendaItem was invoked */
    calledBy = nil
    
    /*
     *   Execute this item.  This is invoked during the actor's turn when the
     *   item is the first item that's ready to execute in the actor's agenda
     *   list.  We do nothing by default.
     *    
     */
    invokeItem() { }

    /*
     *   Reset the item.  This is invoked whenever the item is added to an
     *   actor's agenda.  By default, we'll set isDone to nil as long as
     *   isDone isn't a method; this makes it easier to reuse agenda
     *   items, since we don't have to worry about clearing out the isDone
     *   flag when reusing an item. 
     */
    resetItem()
    {
        /* if isDone isn't a method, reset it to nil */
        if (propType(&isDone) != TypeCode)
            isDone = nil;
    }
    
    /*  
     *   Carry out any additional registration when we're added to our actor's
     *   agenda. We do nothing here, but this is needed on FollowAgendaItem.
     */
    registerItem() { }
    
    /* An optional tag, specified as a single-quoted string. */    
    name = nil
    
    /* 
     *   A convenience method that can be used from within our invokeItem to
     *   display some text only if the player character can see us (or, if the
     *   optional second parameter is supplied, sense us through some other
     *   sense, e.g. &canHear or &canSmell).
     */
    report(msg, prop=&canSee) { senseSay(msg, getActor, prop); }
;


/*
 *   A "conversational" agenda item.  This type of item is ready to execute
 *   only when the actor hasn't engaged in conversation during the same
 *   turn.  This type of item is ideal for situations where we want the
 *   actor to pursue a conversational topic, because we won't initiate the
 *   action until we get a turn where the player didn't directly talk to
 *   us.  
 */
class ConvAgendaItem: AgendaItem
    isReady = (getActor().convAgendaReady(otherActor) && inherited())

    /* 
     *   The actor we're planning to address - by default, this is the PC.
     *   If the conversational overture will be directed to another NPC,
     *   you can specify that other actor here. 
     */
    otherActor = (gPlayerChar)
    
    /* There's more work to do on a ConvAgendaItem when it's invoked */
    invokeItemBase(caller)
    {
        /* 
         *   Note that our actor is in conversation with the otherActor
         *   (normally gPlayerChar) and attempt an actor greeting, if one has
         *   been defined. It's useful to do this here since a ConvAgendaItem
         *   might very well initiate a conversation.
         */        
        local actor = getActor();
        
        /* Set a flag to show why this ConvAgendaItem has just been invoked */
        if(otherActor == gPlayerChar)                    
        {
            /* 
             *   If our actor is not already the player character's current
             *   interlocutor, then we've been invoked to start a new
             *   conversation
             */
            if(gPlayerChar.currentInterlocutor != actor)
                reasonInvoked = InitiateConversationReason;
            
            /*  
             *   Otherwise if we've been invoked from the actor object, note
             *   that we've been invoked from our actor's agendaList during a
             *   lull in the conversation.
             */
            else if(caller == actor)
                reasonInvoked = ConversationLullReason;
            
            /*  
             *   Otherwise note that we've been invoked from a
             *   DefaultAgendaTopic
             */
            else
                reasonInvoked = DefaultTopicReason;
            
            /* 
             *   Give the actor the chance to say hello, and note whether this
             *   resulted in any greeting being displayed. Note that
             *   Actor.actorSayHello() won't actually attempt to do anything if
             *   a conversation is already in progress.
             */
            greetingDisplayed = actor.actorSayHello();    
            
            /* 
             *   Note that our actor has conversed with the player character on
             *   this turn.
             */
            actor.noteConversed();
        }
        else
            otherActor.currentInterlocutor = actor;
        
        /* Then call the base handling */
        inherited(caller);     
        
        /* 
         *   It's possible that our invokeItem() method just tried to set up a
         *   convnode, but since the actor won't have gone through it's
         *   handleTopic method, it won't have moved any pendingKeys into the
         *   activeKeys, so we need to do that now. At the same time we need to
         *   tell the actor not to keep the pending keys beyond the next
         *   conversational turn. But we don't need to do any of this if we've
         *   been invoked from a DefaultAgendaTopic, since in that case we're
         *   being invoked as part of a conversational turn, which will handle
         *   the pending and active keys in any case.
         */
        
        if(reasonInvoked != DefaultTopicReason)
        {
            actor.activeKeys = actor.pendingKeys;        
            actor.keepPendingKeys = nil;
        }
    }
    
    /* 
     *   Flag; did invoking this item result in the display of a greeting (from
     *   an ActorHelloTopic)?
     */    
    greetingDisplayed = nil
    
    /* 
     *   Why was this ConvAgenda Item invoked?
     *.    1 = InitiateConversationReason = Actor initiating new conversation
     *.    2 = ConversationLullReason = Actor using lull in conversation
     *.    3 = DefaultTopicReason = Actor responding to DefaultAgendaTopic
     */            
    reasonInvoked = 0
    
;

/*
 *   A delayed agenda item.  This type of item becomes ready to execute
 *   when the game clock reaches a given turn counter.  
 */
class DelayedAgendaItem: AgendaItem
    /* we're ready if the game clock time has reached our ready time */
    isReady = (libGlobal.totalTurns >= readyTime && inherited())

    /* the turn counter on the game clock when we become ready */
    readyTime = 0

    /*
     *   Set our ready time based on a delay from the current time.  We'll
     *   become ready after the given number of turns elapses.  For
     *   convenience, we return 'self', so a delayed agenda item can be
     *   initialized and added to an actor's agenda in one simple
     *   operation, like so:
     *   
     *   actor.addToAgenda(item.setDelay(1)); 
     */
    setDelay(turns)
    {
        /* 
         *   initialize our ready time as the given number of turns in the
         *   future from the current game clock time 
         */
        readyTime = libGlobal.totalTurns + turns;

        /* return 'self' for the caller's convenience */
        return self;
    }
;


/*
 *   A special kind of agenda item for monitoring "boredom" during a
 *   conversation.  We check to see if our actor is in a conversation, and
 *   the PC has been ignoring the conversation for too long; if so, our
 *   actor initiates the end of the conversation, since the PC apparently
 *   isn't paying any attention to us. 
 */
class BoredomAgendaItem: AgendaItem
    /* we construct these dynamically during actor initialization */
    construct(actor)
    {
        /* remember our actor as our location */
        location = actor;
    }

    /* 
     *   we're ready to run if our actor is in an InConversationState and
     *   its boredom count has reached the limit for the state 
     */
    isReady()
    {
        local actor = getActor();
        local state = actor.curState;
        if(state == nil)
            state = actor;

        return (inherited()
                && gPlayerChar.currentInterlocutor == actor
                && state.attentionSpan != nil
                && actor.boredomCount >= state.attentionSpan);
    }

    /* on invocation, end the conversation */
    invokeItem()
    {
        local actor = getActor();
        local state = actor.curState;
        if(state == nil)
            state = actor;

        /* tell the actor to end the conversation */
        actor.endConversation(endConvBoredom);
       
        /* 
         *   Mark us as done; we'll be reinstated the next time someone
         *   converses with our actor.
         */
        isDone = true;
    }

    /* 
     *   by default, handle boredom before other agenda items - we do this
     *   because an ongoing conversation will be the first thing on the
     *   NPC's mind 
     */
    agendaOrder = 50
;

/* 
 *   An AgendaItem that can be used to trigger actor travel when the actor is
 *   waiting for the player character to follow him/her/it.
 */
class FollowAgendaItem: AgendaItem    
    
    invokeItem()
    {
         /* Note our actor */
        local actor = getActor;
               
        /* Note our actor's current ActorState */
        local stat = actor.curState;
        
        /* If we've exhausted our list of connectors, then we're done. */
        if(nextConnNum >= connectorList.length)    
        {
            isDone = true;    
            actor.followAgendaItem = nil;
            return;
        }
        
        /* Get our next connector */
        local conn = nextConnector;       
        
        /* Let our actor know we're its currently active FollowAgendaItem */
        actor.followAgendaItem = self;
        
        /* 
         *   Travel via the next connector in our list if we're ready to move;
         *   we're ready to move when the player character has just issued a
         *   Follow command, which in turn sets the fuse to move the player
         *   character when we move.
         */
        if(getActor.followFuseID != nil) 
        {           
            /* Note our current actor's current location. */
            local oldLoc = actor.getOutermostRoom;
            
            /* Travel via the connector. */
            conn.travelVia(actor);
            
            /* 
             *   Check whether we actually went anywhere; it's possible the
             *   connector blocked our travel
             */
            if(actor.getOutermostRoom == oldLoc)
            {
                /* 
                 *   Note that our travel was blocked, and check whether the
                 *   travelBlocked method displayed anything.
                 */
                travelBlockReported = gOutStream.watchForOutput(
                    { : travelBlocked(conn)});
                
                /* Note that our actor hasn't actually gone anywhere. */
                actor.lastTravelInfo = nil;
                
            }
            /* Travel was successful */
            else
            {                
                /* Increment our next connector number. */
                nextConnNum++;
                
                /* Note that we've traveled on this turn */
                traveledThisTurn = libGlobal.totalTurns;
                
                /* 
                 *   Mark this Agenda Item as done if we've exhausted our list
                 *   of connectors
                 */
                if(nextConnNum >= connectorList.length)
                {
                    isDone = true;                           
                }
            }
        }
        
        /* 
         *   If we're not ready to move, take the opportunity to "fidget", i.e.
         *   to execute a script defined either on this AgendaItem or on our
         *   current ActorState, provided that our actor hasn't conversed this
         *   term and that the player character can see our actor.
         */
        else if(!actor.conversedThisTurn && Q.canSee(gPlayerChar, actor))
        {
            /* 
             *   If we have a Script class in our Superclass list, execute our
             *   script.
             */
            if(ofKind(Script))
                doScript();
            
            /*  
             *   Otherwise if our actor's current ActorState includes a Script
             *   class in its superclass list, execute its script.
             */
            else if(stat != nil && stat.ofKind(Script))
                stat.doScript();      
        }
        

    }
    
    /* Which turn did this FollowAgendaItem last cause our NPC to travel on? */
    traveledThisTurn = nil
    
    /* A pointer to the next connector to use */
    nextConnNum = 0
    
        
    /* 
     *   A list of TravelConnectors through which we want the player character
     *   to follow our associated actor.
     */
    connectorList = []
    
    /*  The next connector our NPC wants to lead the PC via */
    nextConnector = connectorList.element(nextConnNum + 1)
    
    /* 
     *   This method is called when our NPC attempts to travel via conn but the
     *   travel is blocked (by a locked door, for example). By default we do
     *   nothing here but game code can override this method to display an
     *   appropriate message or take any other action that might be needed in
     *   this situation.
     *
     *   If this method displays anything, the default "You wait in vain for the
     *   traveler to go anywhere" message will be suppressed.
     */
    travelBlocked(conn)
    {
    }
    
    /* 
     *   Flag: did the travelBlocked() method output any text? This is handled
     *   internally by the library and shouldn't normally be used by game author
     *   code.
     */
    travelBlockReported = nil
    
    
    /*   
     *   This method is invoked when our NPC arrives at his/her destination. By
     *   default we do nothing, but instances can override to provide code to
     *   handle the arrival, e.g. by changing the NPC's ActorState.
     */
    noteArrival() { }
    
    resetItem()
    {
        /* Carry out the inherited handling. */
        inherited;
              
        /* Reset our next connector pointer */
        nextConnNum = 0;
    }
    
    /* 
     *   The specialDesc to display when our actor is waiting for the PC to
     *   follow it. By default we just show a plain vanilla message to the
     *   effect, "The NPC is waiting for you to follow him/her north" or
     *   whatever, but game code may wish to override this to provide a more
     *   customized message.
     */
    specialDesc()
    {
        /* 
         *   Note our actor and the player character and create a couple of
         *   useful message parameter substitutions.
         */
        local myactor = getActor;
        local pc = gPlayerChar;       
        gMessageParams(myactor, pc);
        
        /*   
         *   Display our default message. We make use there is a nextDirection
         *   before we attempt to use it in our message, otherwise we simply use
         *   a bland "X is here."
         */
        local nd = nextDirection;
        
        if(nd != nil)
            DMsg(waiting for follow, '{The subj myactor} {is} waiting for {the
                pc} {dummy} to follow {him myactor} {1}. ', nd.departureName);
        else
            DMsg(actor is here, '{The subj myactor} {is} {here}. '); 
    }
        
    nextDirection = getActor.getOutermostRoom.getDirection(nextConnector)
    
    /* 
     *   The specialDesc to use when our NPC has just traveled as a result of
     *   this TravelAgendaItem. By default we just show our specialDesc, but
     *   game code might want to customize this to something like "Bob crosses
     *   the room and waits for you to follow him through the green door."
     */
    arrivingDesc() { specialDesc; }
    
    /* 
     *   Show a specialDesc for this NPC when this TravelAgendaItem is active.
     *   If we've just moved this turn we display the arrivingDesc(), otherwise
     *   we show the specialDesc.
     */
    showSpecialDesc()
    {
        if(traveledThisTurn == libGlobal.totalTurns)
            arrivingDesc();
        else
            specialDesc();        
    }
    
    /* 
     *   Display a message to say that our actor is leaving via conn. This would
     *   normally describe the player character following our actor via conn.
     */
    sayDeparting(conn)
    {
        /* 
         *   By default we use the connector's standard sayActorFollowing()
         *   method.
         */
        conn.sayActorFollowing(gActor, getActor);
    }
    
    
    /* 
     *   Give this AgendaItem the opportunity to react to travel; in particular
     *   this might be used to allow the NPC to react to or even forbid travel
     *   in a direction other than the one s/he's trying to lead the PC.
     */
    beforeTravel(traveler, connector) { }
    
    /* Cancel this FollowAgendaItem before its normal termination. */
    cancel()
    {
        /* Note our actor */
        local actor = getActor;
        
        /* Note that we're done. */
        isDone = true;        
        
        /* Note that we're no longer our actor's current FollowAgendaItem */
        if(actor.followAgendaItem == self)
            actor.followAgendaItem = nil;
        
        /* Remove us from all agenda lists. */
        actor.removeFromAllAgendas(self);        
    }
    
    /*   
     *   Give this agendaItem a high priority to make sure it is used in
     *   response to a FOLLOW ccmmand in preference to any other AgendaItems
     *   that may be pending.
     */
    agendaOrder = 1
    
    registerItem()
    {
        /* Let our actor know we're its currently active FollowAgendaItem */
        getActor.followAgendaItem = self;
    }
;


/* 
 *   An AgendaItem initializer.  For each agenda item that's initially
 *   active, we'll add the item to its actor's agenda.  
 */
PreinitObject
    execute()
    {
        forEachInstance(AgendaItem, function(item) {
            /* 
             *   If this item is initially active, add the item to its
             *   actor's agenda. 
             */
            if (item.initiallyActive)
                item.getActor().addToAgenda(item);
             
                        
        });
    }
;

/* 
 *   Create and store a table of string representation of object names that
 *   might be needed in conversation tags.
 */
objTablePreinit: PreinitObject
    execute()
    {
        t3GetGlobalSymbols().forEachAssoc( new function(key, value)
        {
            if(dataType(value) == TypeObject && (value.ofKind(Mentionable) ||
                                                 value.ofKind(AgendaItem) ||
                                                 value.ofKind(ActorState)))
                
                conversationManager.objNameTab[key] = value;
            
        } );        
    }
    
    executeBeforeMe = [pronounPreinit, thingPreinit]   
;




/* 
 *   A special lister to display a topic inventory list from a list of topics
 *   provided in the lst parameter.
 */
suggestedTopicLister: object

    /* Introduce the topic inventory listing */
    showListPrefix(lst, pl, explicit)  
    { 
        /* 
         *   Introduce the list. If it wasn't explicitly requested start by
         *   outputting a paragraph break and an opening parenthesis.
         */
        if(!explicit)
            "<.p>(";
        
        /* 
         *   Then introduce the list of suggestions with the appropriate form of
         *   'You could' (suitably adjusted for the person of the player
         *   character)
         */
        gCommand.actor = gPlayerChar;
        DMsg(suggestion list intro, '{I} could ');
    }
    
    /* End the list with a closing parenthesis or full stop as appropriate */
    showListSuffix(lst, pl, explicit)  
    { 
        /* 
         *   Finish the list. If it was explicitly requested we finish it with a
         *   full stop and a newline, otherwise we finish it with a closing
         *   parenthesis and a newline.
         */
        if(explicit)
            ".\n";
        else
            ")\n";
    }
    
    /* The message to display if there are no topics to suggest. */
    showListEmpty(explicit)  
    { 
        gCommand.actor = gPlayerChar;
        if(explicit)
            DMsg(nothing in mind, '{I} {have} nothing in mind to discuss
                with {1} just {then}. ',
                 gPlayerChar.currentInterlocutor.theObjName);
    }
    
    show(lst, explicit = true)
    {
        /* 
             *   first exclude all items that don't have a name property, since
             *   there won't be anything to show.
         */        
        lst = lst.subset({x: x.name != nil && x.name.length > 0});
        
        /* 
         *   if the list is empty there's nothing for us to say, so say so and
         *   finish
         */        
        if(lst.length == 0)
        {
            showListEmpty(explicit);                
            return;
        }
        
        
        /* 
         *   Next we need to divide the list according to category
         *
         *
         *
         *   First we go through the list of suggestion types assigning each
         *   topic entry to the user specified type where the user has specified
         *   that it should be suggested at a specific type of Topic Entry (as
         *   opposed to the library default).
         */
        
        foreach(local cur in typeInfo)
        {
            /* 
             *   For each item in our typeInfo list (for which see below),
             *   extract that subset of topic entries from our main list for
             *   which the suggestAs property matches the suggestAs property in
             *   cur (which is the third element of cur). Set the corresponding
             *   list on this lister object (for which we obtain a property
             *   pointer from the first element of cur) to be that subset.
             */
            self.(cur[1]) = lst.subset({t: t.suggestAs == cur[3]});
            
            /* 
             *   Then remove the subset we've just identified from the list of
             *   topic entries to be processed, since we've just accounted for
             *   them
             */
            lst -= self.(cur[1]);
        }
        
        /* 
         *   Then go through every remaining item in our main list, assigning it
         *   to a sublist on the basis of which type of topic entry it is, which
         *   we'll determine on the basis of the property pointers in its
         *   includeInList.
         */        
        foreach(local cur in typeInfo)
        {
            /* 
             *   For each entry in our typeInfo list, find that subset of our
             *   list of topic entries that corresponds to the typeInfo. A topic
             *   entry will correspond to the cur typeInfo if the second element
             *   of cur (a property pointer such as &askTopics) can be found in
             *   the includeInList of the topic entry, which we can test with
             *   our includes method (defined below). Add the subset thus
             *   created to the list contained in the property defined by the
             *   property pointer held in the first element of cur (e.g.
             *   &sayList), which will be a property of this lister object.
             */
            self.(cur[1]) += lst.subset({t: includes(t, cur[2])});
            
            /* 
             *   Remove the sublist we've just created from our overall list of
             *   topic entries, since it's now accounted for.
             */
            lst -= self.(cur[1]);
        }
      
        
        /* 
         *   Introduce the list.
         */
        showListPrefix(lst, nil, explicit);
        
        /* Note that we haven't listed any items yet */
        local listStarted = nil;
        
        /* 
         *   Note that the actor we're listing suggestions for is the player
         *   character's current interlocutor.
         */
        local interlocutor = gPlayerChar.currentInterlocutor;
        
        /* Create a message parameter substitution for the interlocutor */
        gMessageParams(interlocutor);
        
        foreach(local cur in typeInfo)
        {
            listStarted = showSection(cur[1], listStarted, cur[4]) ||
                listStarted;
        }
        
        
        
        
        /* 
         *   Finish the list by appending its suffix 
         */
        showListSuffix(lst, nil, explicit);
        
    }
    
    
    showSection(prop, listStarted, sectionIntro)
    {
        local lst = self.(prop);
        
        /* 
         *   If the list is empty return nil to tell our caller we haven't
         *   displayed anything.
         */
        if(lst.length == 0)
            return nil;
        
        /* If the list has already begun, show our list separator */
        if(listStarted)
            say(orListSep);
        
        /* Show the appropriate intro for this section. */
        if(sectionIntro)
            say(self.(sectionIntro));
        
        /* Show the list */
        showList(lst, sectionIntro);
        
        /* Tell our caller we've displayed something. */
        return true;       
    }
    
    
    /* Show one of our sublists of particular kinds of suggest topics */
    showList(lst, sectionIntro)
    {
        local prfx = sectionIntro ? self.(sectionIntro) + ' ' : '';
        
        /* Sort the list */
        lst = lst.sort(SortAsc, {x, y: x.listOrder - y.listOrder});
        
        
        /* For each element in the list */
        for(local cur in lst, local i = 1 ;; ++i)
        {
            /* 
             *   If the current topic entry wants to include a sayPrefix,
             *   displat the sayPrefix. In practice this only applies to
             *   SayTopics which may or may not want to introduce the name of a
             *   suggestion with 'say'.
             */
            if(cur.includeSayInName)
                say(sayPrefix);
            
            /* Display the name of the current suggestion */
            if(hyperlinkSuggestions)
                "<<aHref(prfx + cur.name, cur.name)>>";
            else
               say(cur.name);
            
            
            /* Output a comma or 'or', depending where we are in the list */
            if(i == lst.length - 1)
                DMsg(or, ' or ');
            if(i < lst.length - 1)
                ", ";
            
        }
    }
    
    /* 
     *   Do we want the topic suggestions to be hyperlinked so that the player
     *   can just click on them? By default we don't but game code can override/
     */
    hyperlinkSuggestions = nil
    
    /* 
     *   The typeInfo contains a list of lists that are used by the show method
     *   to build our various sublists. The first element of each list is a
     *   pointer to the list property to use on this lister object to hold the
     *   particular sublist. The second element of each list is a property
     *   pointer used to identify which sublist a TopicEntry belongs in,
     *   according to its own includeInList property. The third element is the
     *   type of topic entry a topic entry should be suggested as if it is
     *   explicitly requested in its suggestAs property. The fourth element is
     *   the text to use when introducing the corresponding section of the list
     *   (or nil if no introductory text is required).
     *
     */
    typeInfo = [
        [&sayList, &sayTopics, SayTopic, nil],
        [&queryList, &queryTopics, QueryTopic, &queryPrefix],        
        [&askList, &askTopics, AskTopic, &askPrefix],
        [&tellList, &tellTopics, TellTopic, &tellPrefix],
        [&talkList, &talkTopics, TalkTopic, &talkPrefix], 
        [&giveList, &giveTopics, GiveTopic, &givePrefix],
        [&showToList, &showTopics, ShowTopic, &showPrefix],
        [&askForList, &askForTopics, AskForTopic, &askForPrefix],
        [&yesList, &miscTopics, YesTopic, nil],
        [&noList, &miscTopics, NoTopic, nil],
        [&commandList, &commandTopics, CommandTopic, &tellToPrefix]
        
    ]
    
    
    /* Sublists of each kind of suggestion which can be listed in turn */
    sayList = []
    queryList = []
    askList = []
    tellList = []
    talkList = []
    giveList = []
    showToList = []
    yesList = []
    noList = []
    askForList = []
    commandList = []    
    
    
    /* 
     *   Test whether the topicEntry t includes prop in its includeInList.
     */    
    includes(t, prop)
    {
        return t.includeInList.indexOf(prop) != nil;
    }
    
    /* 
     *   The prefix to use when suggesting a SayTopic, if it explicitly wants
     *   the suggestion to start with 'say'.
     */
    sayPrefix = BMsg(say prefix, 'say ')
    queryPrefix = BMsg(ask query, 'ask {him interlocutor} ')
    askPrefix = BMsg(ask about, 'ask {him interlocutor} about ')
    tellPrefix = BMsg(tell about, 'tell {him interlocutor} about ')
    talkPrefix = BMsg(talk about, 'talk about ')    
    givePrefix = BMsg(give, 'give {him interlocutor} ')
    showPrefix = BMsg(show, 'show {him interlocutor} ')
    askForPrefix = BMsg(ask for, 'ask {him interlocutor} for ')
    tellToPrefix = BMsg(tell to, 'tell {him interlocutor} to ')
    
    /*  The conjunction to use at the end of a list of alternatives */
    orListSep = BMsg(or list separator, '; or ')

;

modify Follow
    /* For this action to work all known actors also need to be in scope */
    addExtraScopeItems(whichRole?)
    {
        scopeList = scopeList.appendUnique(Q.knownScopeList.subset({x:
            x.ofKind(Actor)}));
    }
;

/* 
 *   The ProxyActor class can be used to continue the definition of a complex
 *   actor over more than one file. Simply place a ProxyActor object at the head
 *   of a second or subsequent file for the same actor and then set its location
 *   to the Actor object its standing in for. You can then locate TopicGroups,
 *   TopicEntries, ActorStates, AgendaItems, ConvNodes etc., within the
 *   ProxyActor just as if it were the Actor.
 */

class ProxyActor: object
    /* My Actor is my location */
    getActor = location.getActor
    
    /* Pass a call to addTopic() up to my actor */
    addTopic(obj)
    {
        getActor.addTopic(obj);
    }
    
    /* I am active if my location (i.e. my actor) is active). */
    active = location.active
    
    /* 
     *   Set the location to the Actor I'm standing in for in a new file. This
     *   can be done via the @ notation in the template.
     */
    location = nil
;
Adv3Lite Library Reference Manual
Generated on 01/03/2024 from adv3Lite version 1.6.2