sensory.t

documentation
#charset "us-ascii"

#include <tads.h>
#include "advlite.h"

/*
 *   sensory.t
 *
 *   The SENSORY EXTENSION is intended for use with the adv3Lite library. It
 *   adds slightly more sophisticated handling for smells and sounds, as well as
 *   a new SensoryEvent class which can be useful for making actors and other
 *   objects respond to sensory events.
 *
 *   VERSION 2
 *.  19-Jul-14
 *
 *   Usage: include this extension after the adv3Lite library but before your
 *   own game-specific files. Make sure that events.t is also included in your
 *   build.
 */


sensoryID: ModuleID
    name = 'Sensory'
    byline = 'by Eric Eve'
    htmlByline = 'by Eric Eve'
    version = '2'    
;

property remoteSmellDesc;
property remoteListenDesc;
property tooFarAwayToHearMsg;
property tooFarAwayToSmellMsg;


/* 
 *   The SensoryEmanation class is the base class for sensory emanations such as
 *   smells and noises. [MODIFIED FOR SENSORY EXTENSION]
 */
modify SensoryEmanation
    
    /* 
     *   By default we vary our description according to whether the player
     *   character can see the object whose sound or smell we represent. If you
     *   don't these this variation, you can just override desc directly.
     *   [MODIFIED FOR SENSORY EXTENSION]
     */
    desc
    {
        if(Q.canSee(gPlayerChar, location))
            descWithSource;
        else
            descWithoutSource;
    }
    
    /* 
     *  Our description when the player character can see our source 
     *  [DEFINED IN SENSORY EXTENSION]
     */
    descWithSource = nil
    
    /* 
     *  Our description when the player character can't see our source 
     *  [DEFINED IN SENSORY EXTENSION]
     */
    descWithoutSource = nil
    
    /*  
     *   Are we actually emanating? We may not be if something stops us, e.g.
     *   breaking a ticking clock [DEFINED IN SENSORY EXTENSION]
     */
    isEmanating = true
    
    /*  
     *  If we're not emanating we can't be sensed at all, so we're hidden 
     *  [DEFINED IN SENSORY EXTENSION]
     */
    isHidden = !isEmanating
    
    /* 
     *   The emanate method is called on each turn that the player character can
     *   sense us, and can be used to display a message announcing our presence,
     *   such as "There's an awful stink here" or "A loud ticking noise comes
     *   from somewhere. " [DEFINED IN SENSORY EXTENSION]
     */
    emanate() 
    { 
        /* If our schedule is nil (see below) we don't do anything at all */
        if(schedule == nil)
            return;
        
        /* Get the interval before we're due to show another message */
        local interval = schedule[scheduleState];
        
        /* 
         *   If this is nil, we don't want to show another message, so simply
         *   end the routine here.
         */
        if(interval == nil)
            return;
        
        /*  
         *   If incrementing our emanation state by one makes it greater than
         *   the its value when we last showed a message plus the interval to
         *   the next one, show our emanation desc
         */ 
        if(++emanationState >= lastEmanationTime + interval)
        {
            /* Display our emanation description */
            emanationDesc();
            
            /* 
             *   If we haven't reached the end of the emanation schedule yet,
             *   increase our scheduleState by one.
             */
            if(nilToList(schedule).length > scheduleState)
                scheduleState++;
        
            /*  Note when we last displayed a message. */
            lastEmanationTime = emanationState;
            
            /*  Note that the player character must now know about us. */
            setKnown();
        }
    }
    
    /* 
     *   The message to display to announce our presence. This is overridden on
     *   our subclasses.[DEFINED IN SENSORY EXTENSION]
     */
    emanationDesc() { }
    
    /*  
     *   A counter to keep track of when we're next due to display an emanation
     *   message [DEFINED IN SENSORY EXTENSION]
     */
    emanationState = 0
    
    /*  
     *  A counter to keep track of where we are in our emanation schedule. 
     *  [DEFINED IN SENSORY EXTENSION]
     */
    scheduleState = 1
    
    /*  
     *  The last time we emanated, relevant to when we started emanating 
     *  [DEFINED IN SENSORY EXTENSION]
     */
    lastEmanationTime = 0
    
    /*  
     *   Our emanation schedule. If this is just nil we won't show any emanation
     *   meessages at all. Otherwise this should be a list of numbers. The first
     *   number is the first interval between emanations, the second number the
     *   second interval and so on. When we get to the end of the list we keep
     *   using the last number in the list as the interval. If the last entry in
     *   the list is nil we stop showing emanation messages. This can be used to
     *   reduce the frequency of messages to model the player character becoming
     *   less aware of us. [DEFINED IN SENSORY EXTENSION]
     */
    schedule = [1]

    /*  Reset all out counters to their initial states [DEFINED IN SENSORY EXTENSION] */
    reset()
    {
        emanationState = 0;
        scheduleState = 1;
        if(ofKind(Script))
            curScriptState = 1;
        lastEmanationTime = 0;
    }
       
    /* 
     *   The message to display when the player tries to do something with us
     *   other than sense us. [DEFINED IN SENSORY EXTENSION]
     */
    notImportantMsg = BMsg(cannot do to sensory, '{I} {can\'t} do that to {a
        cobj}. ')
;

/* An Odor is a SensoryEmanation representing a Smell [MODIFIED FOR SENSORY EXTENSION]*/
modify Odor
        
    /*   
     *   The message to be displayed to show that there's a smell here. The
     *   default implementation should be serviceable in many cases, but game
     *   code can easily override this method if something different is
     *   required. By default we execute our script if we are one, otherwise we use
	 *   our own smellDesc or remoteSmellDesc as appropriate.
	 *   [DEFINED IN SENSORY EXTENSION]
     */
    emanationDesc()
    { 
        /* 
         *   If we're mixed in with an EventList class, display the next item
         *   from our eventList.
         */
        if(ofKind(Script))
            doScript();
        /*  
         *   Otherwise use our location's smellDesc or remoteSmellDesc, as
         *   appropriate
         */
        else
        {
            if(!location.isIn(gPlayerChar.getOutermostRoom) 
               && location.propDefined(&remoteSmellDesc))
                location.remoteSmellDesc(gPlayerChar);
            else
                location.smellDesc;
        }
    }
    
    /* 
     *   Only carry out the inherited handling if the player hasn't issued a
     *   SMELL command on this turn, otherwise there's the risk of duplicate
     *   messages. [DEFINED IN SENSORY EXTENSION]
     */
    emanate()
    {
        if(!gActionIn(Smell, SmellSomething))
            inherited;
    }    
;

/*  
 *   A SimpleOdor is an object representing a free-standing smell directly
 *   present in a location rather than attached to any specific object. It can
 *   be used to display atmospheric smells either according to its schedule or
 *   in response to a SMELL command. [DEFINED IN SENSORY EXTENSION]
 */
class SimpleOdor: Odor
    /*  
     *   Unless this is overridden, our desc property simply executes our
     *   script. [DEFINED IN SENSORY EXTENSION]
     */
    desc() { doScript(); }
    
    /* The smellDesc of a SimpleOdor is simply its desc.[DEFINED IN SENSORY EXTENSION] */
    smellDesc = desc
      
    /* 
     *   A SimpleOdor is a prominent smell by default, since we want it to show
     *   up in response to a SMELL command. [DEFINED IN SENSORY EXTENSION]
     */
    isProminentSmell = true
    
	/*   
     *   The message to be displayed to show that there's a smell here. 
     *   We display either our own smellDesc or our remoteSmellDesc,
     *   as appropriate.
     *   DEFINED IN SENSORY EXTENSION]
     */
    emanationDesc()
    { 
        
        if(!isIn(gRoom) && propDefined(&remoteSmellDesc))
            remoteSmellDesc(gPlayerChar);
        else
            smellDesc;        
    }
    
;

/* An Noise is a SensoryEmanation representing a Sound [MODIFIED FOR SENSORY EXTENSION] */
modify Noise   
    
    /*   
     *   The message to be displayed to show that there's a sound here. The
     *   default implementation should be serviceable in many cases, but game
     *   code can easily override this method if something different is
     *   required. By default we execute our script if we are one, otherwise we use
	 *   our own listenDesc or remoteListenDesc as appropriate.
	 *   [DEFINED IN SENSORY EXTENSION]
     */
    emanationDesc()
    { 
        /* 
         *   If we're mixed in with an EventList class, display the next item
         *   from our eventList.
         */
        if(ofKind(Script))
            doScript();
        /*  
         *   Otherwise use our location's listenDesc, or remoteListenDesc, as
         *   appropriate
         */
        else
        {
            if(!location.isIn(gPlayerChar.getOutermostRoom) 
               && location.propDefined(&remoteListenDesc))
                location.remoteListenDesc(gPlayerChar);
            else
                location.listenDesc;
        }
    }
    
    /* 
     *   Only carry out the inherited handling if the player hasn't issued a
     *   LISTEN command on this turn, otherwise there's the risk of duplicate
     *   messages. [DEFINED IN SENSORY EXTENSION]
     */
    emanate()
    {
        if(!gActionIn(Listen, ListenTo))
            inherited;
    }  
;

/*  
 *   A SimpleNoise is an object representing a free-standing sound directly
 *   present in a location rather than attached to any specific object. It can
 *   be used to display atmospheric sounds either according to its schedule or
 *   in response to a LISTEN command. [DEFINED IN SENSORY EXTENSION]
 */
class SimpleNoise: Noise
    /*  
     *   Unless this is overridden, our desc property simply executes our
     *   script. [DEFINED IN SENSORY EXTENSION]
     */
    desc() { doScript(); }
    
    /* The listenDesc of a SimpleNoise is simply its desc.[DEFINED IN SENSORY EXTENSION]  */
    listenDesc = desc
     
    /* 
     *   A SimpleNoise is a prominent noise by default, since we want it to show
     *   up in response to a LISTEN command.
     */
    isProminentNoise = true
 
    /*   
     *   The message to be displayed to show that there's a noise here. 
     *   We display either our own listenDesc or our remoteListenDesc,
     *   as appropriate.
     *   [DEFINED IN SENSORY EXTENSION]
     */
    emanationDesc()
    { 
        
        if(!isIn(gRoom) && propDefined(&remoteListenDesc))
            remoteListenDesc(gPlayerChar);
        else
            listenDesc;        
    }
;


/* 
 *  The object which drives emanation messages for Odors and Noises 
 *  [DEFINED IN SENSORY EXTENSION]
 */
emanationControl: InitObject
    /* Set up our Daemon at the start of play. [DEFINED IN SENSORY EXTENSION] */
    execute()
    {
        new Daemon(self, &emanate, 1);
    }
    
    /* 
     *   Each turn, execute the emanate() method for every item in our list of
     *   emanations. [DEFINED IN SENSORY EXTENSION]
     */
    emanate()
    {
        /* Get a list of items potentially due to emanate */
        local lst = buildEmanationList;
        
        /* Make every item in our list execute its emanate method */
        for(local e in lst)
            e.emanate();      
    }
    
    
    /*  
     *   Construct a list of SensoryEmanations that can currently be sensed by
     *   the player character. [DEFINED IN SENSORY EXTENSION]
     */
    buildEmanationList   
    {
        local pc = gPlayerChar;
        
        /* 
         *   First get a list of all the SensoryEmanations in the player
         *   character's current room that can be sensed by the player character
         */
        local lst = pc.getOutermostRoom.allContents.subset(
            {o: canSense(pc, o)});
        
        /*  
         *   If the SenseRegion class is defined then add all the
         *   SensoryEmanations that can be sensed in remote locations
         */
        if(defined(SenseRegion))
        {
            /* Set up an empty list of remote SensoryEmanations */
            local remoteLst = [];
            
            /* 
             *   For each room that's audible from the player character's
             *   current location, all all the currently emanating Noises that
             *   the player character can hear.
             */
            foreach(local rm in valToList(pc.getOutermostRoom.audibleRooms))
                remoteLst += rm.allContents.subset(
                    {o: o.isEmanating && o.ofKind(Noise) && Q.canHear(pc, o)});
            
            /* 
             *   For each room that's audible from the player character's
             *   current location, all all the currently emanating Odors that
             *   the player character can smell.
             */
            foreach(local rm in valToList(pc.getOutermostRoom.smellableRooms))
                remoteLst += rm.allContents.subset(
                    {o: o.isEmanating && o.ofKind(Odor) && Q.canSmell(pc, o)});
            
            /* 
             *   Add the remote list to the local list, ensuring that each item
             *   appears only once.
             */
            lst = lst.appendUnique(remoteLst);    
        }
        
        /* Return the resulting list. */
        return lst;    
    }
        
    /* 
     *   The pc can sense o if o is currently emanating and its a Noise the pc
     *   can currently hear or an Odor the pc can currently smell. [DEFINED IN SENSORY EXTENSION]
     */   
    canSense(pc, o)
    {
        return o.isEmanating && ((o.ofKind(Noise) && Q.canHear(pc, o))
                                  || (o.ofKind(Odor) && Q.canSmell(pc, o)));
    }   
;

/* 
 *   A SensoryEvent is a brief event in time, such as a sudden noise, to which
 *   other actors or objects in the vicinity may react. [DEFINED IN SENSORY EXTENSION]
 */
class SensoryEvent: object
    /* 
     *   We call the trigger event method of a SensoryEvent to simulate the
     *   occurrence of that event. The obj parameter is the object associated
     *   with the event, for example the source of a sudden explosion.
	 *   [DEFINED IN SENSORY EXTENSION]
     */    
    triggerEvent(obj)
    {
        /* 
         *   Construct a list containing all the items in the player character's
         *   current room that the pc can sense via the relevant sense
         */
        local notifyList = obj.getOutermostRoom.allContents.subset({
            o: Q.(senseProp)(o, obj) });
                
        /* 
         *   Add any items from remote locations that meet the same conditions,
         *   making the list unique so we don't get any duplicates.
         */
        notifyList = notifyList.appendUnique(remoteList(obj));
        
        /*  
         *   Notify every item in our notification list that this event has just
         *   occurred.
         */
        for(local cur in notifyList)
            cur.(notifyProp)(self, obj);
        
        /*  
         *   Presumablty obj has just made its presence known to the player
         *   character, even if it wasn't before.
         */
        obj.setKnown();
    }
    
    /* 
     *   A property pointer to the property on each notified object that needs
     *   to be executed when it's notified of this SensoryEvent (e.g.
     *   &notifySoundEvent). [DEFINED IN SENSORY EXTENSION]
     */
    notifyProp = nil
    
    /*   
     *   The property pointer relating to the Q method that needs to be called
     *   to determined whethet this SensoryEvent can be sensed (e.g. &canHear).
	 *   [DEFINED IN SENSORY EXTENSION]
     */
    senseProp = nil
    
    /*   
     *   The property pointer to the property of Room defining which list of
     *   rooms also needs to be checked for remote items that might sense this
     *   event (e.g. &audibleRooms). [DEFINED IN SENSORY EXTENSION]
     */
    remoteProp = nil
    
    /*  Construct a list of notifiable objects in remote locations  [DEFINED IN SENSORY EXTENSION]*/
    remoteList(obj)
    {
        /* Start with an empty list */
        local lst = [];
        
        /* 
         *   If the SenseRegion class isn't present, there's no point trying to
         *   look for objects in remote rooms.
         */
        if(defined(SenseRegion))
        {
           /* 
            *   Got through each room in the appropriate list of remote rooms
            *   that can be sensed from us through the appropriate sense to
            *   build a list of all their contents which can be senses via the
            *   appropriate sense.
            */
            for(local rm in valToList(obj.getOutermostRoom.(remoteProp)))
                lst = lst.appendUnique(rm.allContents.({
                    o: Q.(senseProp)(o, obj) }));
        }
        
        /* Return the resulting list */
        return lst;
    }
;

/* 
 *   A SoundEvent represents any sudden noise to which other objects or people
 *   might react. [DEFINED IN SENSORY EXTENSION]
 */
class SoundEvent: SensoryEvent    
    notifyProp = &notifySoundEvent
    senseProp = &canHear
    remoteProp = &audibleRooms
    
;

/* 
 *   A SoundEvent represents any sudden smell to which other objects or people
 *   might react. [DEFINED IN SENSORY EXTENSION]
 */
class SmellEvent: SensoryEvent    
    notifyProp = &notifySmellEvent
    senseProp = &canSmell
    remoteProp = &smellableRooms
;


/* 
 *   A SightEvent represents any visible event to which other objects or people
 *   might react. [DEFINED IN SENSORY EXTENSION]
 */
class SightEvent: SensoryEvent    
    notifyProp = &notifySightEvent
    senseProp = &canSee
    remoteProp = &visibleRooms
;

/*  Modifications to Thing to work with the <i>SENSORY EXTENSION</i> */
modify Thing
    /* 
     *   The methods that define our reactions to SoundEvents, SmellEvents and
     *   SightEvents respectively. By default all three methods defer to a
     *   common handler. [DEFINED IN SENSORY EXTENSION]
     */    
    notifySoundEvent(event, source) { notifyEvent(event, source); }
	
	/*  Our reaction to a SmellEvent. By default we defer to the common handler. */
    notifySmellEvent(event, source) { notifyEvent(event, source); }
	
	/*  Our reaction to a SightEvent. By default we defer to the common handler. */
    notifySightEvent(event, source) { notifyEvent(event, source); }
        
    /* 
     *   Our common handler for SensoryEvents; it may often be more convenient
     *   to use this than to write separate handlers for each kind of
     *   SensoryEvent, since in any case the event parameter (containing the
     *   SensoryEvent that's just been triggered) tells us what kind of
     *   SensoryEvent it is. The source parameter is the object associated with
     *   the event. [DEFINED IN SENSORY EXTENSION]
     */
    notifyEvent(event, source) {  }   
    
    /*   
     *   By default we split our smellDesc into smellDescWithoutSource (when the
     *   player character can't see us) and smellDescWithSource (when the pc
     *   can). If we don't need this distinction we can override this method
     *   directly. [MODIFIED FOR SENSORY EXTENSION]
     */
    smellDesc()
    {
      if(Q.canSee(gActor, self))
            smellDescWithSource;
        else
            smellDescWithoutSource;   
    }
  
    /* 
     *  The response to SMELLing this object when the actor can see us. 
     *  [DEFINED IN SENSORY EXTENSION]
     */
    smellDescWithSource = nil

    /* 
     *  The response to SMELLing this object when the actor can't see us. 
     *  [DEFINED IN SENSORY EXTENSION]
     */
    smellDescWithoutSource = nil
    
    /*   
     *   By default we split our listenDesc into listenDescWithoutSource (when
     *   the player character can't hear us) and listenDescWithSource (when the
     *   pc can). If we don't need this distinction we can override this method
     *   directly. [MODIFIED FOR SENSORY EXTENSION]
     */
    listenDesc()
    {
      if(Q.canSee(gActor, self) )
            listenDescWithSource;
        else
            listenDescWithoutSource;   
    }
  
    /* 
     *  The response to LISTENing TO this object when the actor can see us. 
     *  [DEFINED IN SENSORY EXTENSION]
     */
    listenDescWithSource = nil
    
    /* 
     *   The response to LISTENing TO this object when the actor can't see us. 
     *   [DEFINED IN SENSORY EXTENSION]
     */
    listenDescWithoutSource = nil    
    
    /* 
 	 * [MODIFIED FOR SENSORY EXTENSION] 
	 * If I have an associated Noise object which isn't emanating, assume
	 * I have fallen silent, otherwise carry out the inherited handling.
	 */
    dobjFor(ListenTo)
    {
        action()
        {
            /* 
             *   If I have an associated Noise object which isn't emanating,
             *   then assume I have fallen silent.
             */
            
            if(soundObj != nil && !soundObj.isEmanating)
                say(hearNothingMsg);          
            else
                inherited;
        }
    }
    
    /* 
 	 * [MODIFIED FOR SENSORY EXTENSION] 
	 * If I have an associated Odor object which isn't emanating, assume
	 * I no longer smell of anything, otherwise carry out the inherited handling.
	 */
    dobjFor(SmellSomething)
    {
        action()
        {
            /* 
             *   If I have an associated Odor object which isn't emanating,
             *   then assume I no longer smell.
             */            
            if(smellObj != nil && !smellObj.isEmanating)
                say(smellNothingMsg);          
            else
                inherited;
        }
    }
    
    /* 
     *   We don't have a prominent smell if we have an associated Odor object
     *   that isn't emanating. [MODIFIED FOR SENSORY EXTENSION]
     */
    isProminentSmell
    {
        if(smellObj && !smellObj.isEmanating)
            return nil;
        return true;
    }
    
    /* 
     *   We don't have a prominent noise if we have an associated Noise object
     *   that isn't emanating. [MODIFIED FOR SENSORY EXTENSION]
     */
    isProminentNoise
    {
        if(soundObj && !soundObj.isEmanating)
            return nil;
        
        return true;
    }
    
    /* Our associated Odor object, if we have one [SENSORY EXTENSION]*/
    smellObj = (contents.valWhich({o: o.ofKind(Odor)}))
    
    /* Our associated Noise object, if we have one. [SENSORY EXTENSION]*/            
    soundObj = (contents.valWhich({o: o.ofKind(Noise)}))
;

/* MODIFICATIONS FOR SENSORY EXTENSION */
modify Room
    /* 
     *   Reset every SensoryEmanation in this room to its initial state when the
     *   player character leaves this room. [MODIFIED FOR SENSORY EXTENSION]
     */
    notifyDeparture(traveler, dest)
    {
        inherited(traveler, dest);
        
        if(traveler == gPlayerChar)
        {
            local lst = allContents.subset({o: o.ofKind(SensoryEmanation)});             
            
            /* 
             *   If the SenseRegion class is included, then we need to deal with
             *   SensoryEmanations in remote rooms.
             */
            if(defined(SenseRegion))
            {
                local sp = defined(scopeProbe_) ? scopeProbe_ : object: Thing {};
                try
                {                    
                    sp.moveInto(dest);
                    
                    /* First add all the Noises in the remote rooms we can hear */
                    for(local rm in getOutermostRoom.audibleRooms)
                        lst.appendUnique(rm.allContents.subset({o:
                            o.ofKind(Noise)}));
                    
                    /* Then add all the Odors in the remote rooms we can smell */
                    for(local rm in getOutermostRoom.smellableRooms)
                        lst.appendUnique(rm.allContents.subset({o:
                            o.ofKind(Odor)}));
                    
                    /* 
                     *   Finally remove all the Odors that can't be smelled from
                     *   the destination and all the Noises than can't be heard
                     *   from the destination.
                     */
                    
                    lst = lst.subset({o: (o.ofKind(Noise) &&
                                          !Q.canHear(sp, o)) 
                                     || (o.ofKind(Odor) 
                                         && !Q.canSmell(sp, o))});
                }
                finally
                {
                    
                    sp.moveInto(nil);
                }
            }
                
            /* Reset every SensoryEmanation in our list */
            lst.forEach({o: o.reset() });
        }
    }
;
                                       
Adv3Lite Library Reference Manual
Generated on 25/04/2024 from adv3Lite version 2.0