sense.t

documentation
 #charset "us-ascii"
 
 /* 
  *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved. 
  *   
  *   TADS 3 Library - senses
  *   
  *   This module defines objects and functions related to senses.  This
  *   file is language-independent.  
  */
 
 /* include the library header */
 #include "adv3.h"
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Material: the base class for library objects that specify the way
  *   senses pass through objects.  
  */
 class Material: object
     /*
      *   Determine how a sense passes through the material.  We'll return
      *   a transparency level.  (Individual materials should not need to
      *   override this method, since it simply dispatches to the various
      *   xxxThru methods.)
      */
     senseThru(sense)
     {
         /* dispatch to the xxxThru method for the sense */
         return self.(sense.thruProp);
     }
 
     /*
      *   For each sense, each material must define an appropriate xxxThru
      *   property that returns the transparency level for that sense
      *   through the material.  Any xxxThru property not defined in an
      *   individual material defaults to opaque.
      */
     seeThru = opaque
     hearThru = opaque
     smellThru = opaque
     touchThru = opaque
 ;
 
 /*
  *   Adventium is the basic stuff of the game universe.  This is the
  *   default material for any object that doesn't specify a different
  *   material.  This type of material is opaque to all senses.  
  */
 adventium: Material
     seeThru = opaque
     hearThru = opaque
     smellThru = opaque
     touchThru = opaque
 ;
 
 /*
  *   Paper is opaque to sight and touch, but allows sound and smell to
  *   pass.  
  */
 paper: Material
     seeThru = opaque
     hearThru = transparent
     smellThru = transparent
     touchThru = opaque
 ;
 
 /*
  *   Glass is transparent to light, but opaque to touch, sound, and smell.
  */
 glass: Material
     seeThru = transparent
     hearThru = opaque
     smellThru = opaque
     touchThru = opaque
 ;
 
 /*
  *   Fine Mesh is transparent to all senses except touch.  
  */
 fineMesh: Material
     seeThru = transparent
     hearThru = transparent
     smellThru = transparent
     touchThru = opaque
 ;
 
 /*
  *   Coarse Mesh is transparent to all senses, including touch, but
  *   doesn't allow large objects to pass through.  
  */
 coarseMesh: Material
     seeThru = transparent
     hearThru = transparent
     smellThru = transparent
     touchThru = transparent
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Sense: the basic class for senses.  
  */
 class Sense: object
     /*
      *   Each sense must define the property thruProp as a property
      *   pointer giving the xxxThru property for the sense.  The xxxThru
      *   property is the property of a material which determines how the
      *   sense passes through that material.  
      */
     thruProp = nil
 
     /*
      *   Each sense must define the property sizeProp as a property
      *   pointer giving the xxxSize property for the sense.  The xxxSize
      *   property is the property of a Thing which determines how "large"
      *   the object is with respect to the sense.  For example, sightSize
      *   indicates how large the object is visually, while soundSize
      *   indicates how loud the object is.
      *   
      *   The purpose of an object's size in a given sense is to determine
      *   how well the object can be sensed through an obscuring medium or
      *   at a distance.  
      */
     sizeProp = nil
 
     /*
      *   Each sense must define the property presenceProp as a property
      *   pointer giving the xxxPresence property for the sense.  The
      *   xxxPresence property is the property of a Thing which determines
      *   whether or not the object has a "presence" in this sense, which is
      *   to say whether or not the object is emitting any detectable
      *   sensory data for the sense.  For example, soundPresence indicates
      *   whether or not a Thing is making any noise.
      *   
      *   The sensory presence is used to determine if an object is in
      *   scope.  An object with a detectable sensory presence is normally
      *   in scope.  Note that sounds and smells emitted by a tangible
      *   object are frequently represented as additional intangible
      *   objects, and in these cases the intangible object (the sensory
      *   emanation) is usually the object with a sensory presence, rather
      *   than the tangible object making the noise/odor.  However, it is
      *   sometimes obvious that a particular sound or odor is coming from a
      *   particular kind of object, so the presence of the sound or odor
      *   implies the presence of the source object and thus places the
      *   source object in scope.  In such cases, it is desirable for the
      *   source object to have a sensory presence of its own, in addition
      *   to the sensory presence of the intangible sensory emanation
      *   object.
      *   
      *   Note that the "presence" doesn't have any effect on whether or not
      *   an object can be sensed.  Only the sense path matters for that: an
      *   object without a presence can still be sensed if there's a
      *   non-opaque sense path to the object.  Presence only determines
      *   whether or not an object is *actively* calling attention to
      *   itself.  
      */
     presenceProp = nil
 
     /*
      *   Each sense can define this property to specify a property pointer
      *   used to define a Thing's "ambient" energy emissions.  Senses
      *   which do not use ambient energy should define this to nil.
      *   
      *   Some senses work only on directly emitted sensory data; human
      *   hearing, for example, has no (at least effectively no) use for
      *   reflected sound, and can sense objects only by the sounds they're
      *   actually emitting.  Sight, on the other hand, can make use not
      *   only of light emitted by an object but of light reflected by the
      *   object.  So, sight defines an ambience property, whereas hearing,
      *   touch, and smell do not.  
      */
     ambienceProp = nil
 
     /*
      *   Determine if, in general, the given object can be sensed under
      *   the given conditions.  Returns true if so, nil if not.  By
      *   default, if the ambient level is zero, we'll return nil;
      *   otherwise, if the transparency level is 'transparent', we'll
      *   return true; otherwise, we'll consult the object's size:
      *   
      *   - Small objects cannot be sensed under less than transparent
      *   conditions.
      *   
      *   - Medium or large objects can be sensed in any conditions other
      *   than opaque.
      */
     canObjBeSensed(obj, trans, ambient)
     {
         /* 
          *   if we use "reflected" energy, and the ambient energy level is
          *   zero, we can't sense it 
          */
         if (ambienceProp != nil && ambient == 0)
             return nil;
 
         /* check the transparency level */
         switch(trans)
         {
         case transparent:
         case attenuated:
             /* 
              *   we can always sense under transparent or attenuated
              *   conditions 
              */
             return true;
 
         case distant:
         case obscured:
             /* 
              *   we can only sense medium and large objects under less
              *   than transparent conditions 
              */
             return obj.(self.sizeProp) != small;
 
         default:
             /* we can never sense under other conditions */
             return nil;
         }
     }
 ;
 
 /*
  *   The senses.  We define sight, sound, smell, and touch.  We do not
  *   define a separate sense for taste, since it would add nothing to our
  *   model: you can taste something if and only if you can touch it.
  *   
  *   To add a new sense, you must do the following:
  *   
  *   - Define the sense object itself, in parallel to the senses defined
  *   below.
  *   
  *   - Modify class Material to set the default transparency level for
  *   this sense by defining the property xxxThru - for most senses, the
  *   default transparency level is 'opaque', but you must decide on the
  *   appropriate default for your new sense.
  *   
  *   - Modify class Thing to set the default xxxSize setting, if desired.
  *   
  *   - Modify class Thing to set the default xxxPresence setting, if
  *   desired.
  *   
  *   - Modify each instance of class 'Material' that should have a
  *   non-default transparency for the sense by defining the property
  *   xxxThru for the material.
  *   
  *   - Modify class Actor to add the sense to the default mySenses list;
  *   this is only necessary if the sense is one that all actors should
  *   have by default.  
  */
 
 sight: Sense
     thruProp = &seeThru
     sizeProp = &sightSize
     presenceProp = &sightPresence
     ambienceProp = &brightness
 ;
 
 sound: Sense
     thruProp = &hearThru
     sizeProp = &soundSize
     presenceProp = &soundPresence
 ;
 
 smell: Sense
     thruProp = &smellThru
     sizeProp = &smellSize
     presenceProp = &smellPresence
 ;
 
 touch: Sense
     thruProp = &touchThru
     sizeProp = &touchSize
     presenceProp = &touchPresence
 
     /*
      *   Override canObjBeSensed for touch.  Unlike other senses, touch
      *   requires physical contact with an object, so it cannot operate at
      *   a distance, regardless of the size of an object.  
      */
     canObjBeSensed(obj, trans, ambient)
     {
         /* if it's distant, we can't sense the object no matter how large */
         if (trans == distant)
             return nil;
 
         /* for other cases, inherit the default handling */
         return inherited(obj, trans, ambient);
     }
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   "Add" two transparency levels, yielding a new transparency level.
  *   This function can be used to determine the result of passing a sense
  *   through multiple layers of material.  
  */
 transparencyAdd(a, b)
 {
     /* transparent + x -> x for all x */
     if (a == transparent)
         return b;
     if (b == transparent)
         return a;
 
     /* opaque + x -> opaque for all x */
     if (a == opaque || b == opaque)
         return opaque;
 
     /* 
      *   any other combinations yield opaque - we can't have two levels of
      *   attenuation or obscuration without losing all detail and energy
      *   transmission 
      */
     return opaque;
 }
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Compare two transparency levels to determine which one is more
  *   transparent.  Returns 0 if the two levels are equally transparent, 1
  *   if the first one is more transparent, and -1 if the second one is
  *   more transparent.  The comparison follows this rule:
  *   
  *   transparent > attenuated > distant == obscured > opaque 
  */
 transparencyCompare(a, b)
 {
     /*
      *   for the purposes of the comparison, consider obscured to be
      *   identical to distant
      */
     if (a == obscured)
         a = distant;
     if (b == obscured)
         b = distant;
 
     /* if they're the same, return zero to so indicate */
     if (a == b)
         return 0;
 
     /*
      *   We know they're not equal, so if one is transparent, then the
      *   other one isn't.  Thus, if either one is transparent, it's the
      *   winner.
      */
     if (a == transparent)
         return 1;
     if (b == transparent)
         return -1;
 
     /*
      *   We know they're not equal and we know neither is transparent, so
      *   if one is attenuated then the other is worse, and the attenuated
      *   one is the winner.  
      */
     if (a == attenuated)
         return 1;
     if (b == attenuated)
         return -1;
 
     /*
      *   We now know neither one is transparent or attenuated, and we've
      *   already transformed obscured into distant, so the only possible
      *   values remaining are distant and opaque.  We know also they're
      *   not equal, because we would have already returned if that were
      *   the case.  So, we can conclude that one must be distant and the
      *   other must be opaque.  Hence, the one that's opaque is the less
      *   transparent one.  
      */
     if (a == opaque)
         return -1;
     else
         return 1;
 }
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Given a brightness level and a transparency level, compute the
  *   brightness as modified by the transparency level. 
  */
 adjustBrightness(br, trans)
 {
     switch(trans)
     {
     case transparent:
     case distant:
         /* 
          *   Transparent medium or distance - this doesn't modify
          *   brightness at all.  (Technically, distance would reduce
          *   brightness somewhat, but the typical scale of an IF setting
          *   isn't usually large enough that brightness should
          *   significantly diminish.)  
          */
         return br;
 
     case attenuated:
     case obscured:
         /* 
          *   Distant, obscured, or attenuated.  We reduce self-illuminating
          *   light (level 1) and dim light (level 2) to nothing (level 0),
          *   we leave nothing as nothing (obviously), and we reduce all
          *   other levels one step.  So, everything below level 3 goes to
          *   0, and everything at or above level 3 gets decremented by 1.  
          */
         return (br >= 3 ? br - 1 : 0);
 
     case opaque:
         /* opaque medium - nothing makes it through */
         return 0;
 
     default:
         /* shouldn't get to other cases */
         return nil;
     }
 }
 
 /* ------------------------------------------------------------------------ */
 /*
  *   SenseConnector: an object that can pass senses across room
  *   boundaries.  This is a mix-in class: add it to the superclass list of
  *   the object before Thing (or a Thing subclass).
  *   
  *   A SenseConnector acts as a sense conduit across all of its locations,
  *   so to establish a connection between locations, simply place a
  *   SenseConnector in each location.  Since a SenseConnector is useful
  *   only when placed placed in multiple locations, SenseConnector is
  *   based on MultiLoc.  
  */
 class SenseConnector: MultiLoc
     /*
      *   A SenseConnector's material generally determines how senses pass
      *   through the connection.  
      */
     connectorMaterial = adventium
 
     /*
      *   Determine how senses pass through this connection.  By default,
      *   we simply use the material's transparency.
      */
     transSensingThru(sense) { return connectorMaterial.senseThru(sense); }
 
     /*
      *   Add the direct containment connections for this item to a lookup
      *   table. 
      *   
      *   Since we provide a sense connection among all of our containers,
      *   add each of our containers to the list.  
      */
     addDirectConnections(tab)
     {
         /* add myself */
         tab[self] = true;
             
         /* add my contents */
         foreach (local cur in contents)
         {
             if (tab[cur] == nil)
                 cur.addDirectConnections(tab);
         }
 
         /* add my containers */
         foreach (local cur in locationList)
         {
             if (tab[cur] == nil)
                 cur.addDirectConnections(tab);
         }
     }
 
     /*
      *   Transmit energy from a container onto me.
      */
     shineFromWithout(fromParent, sense, ambient, fill)
     {
         /* if this increases my ambient level, accept the new level */
         if (ambient > tmpAmbient_)
         {
             local levelThru;
             
             /* remember the new level and fill material to this point */
             tmpAmbient_ = ambient;
             tmpAmbientFill_ = fill;
 
             /* transmit to my contents */
             shineOnContents(sense, ambient, fill);
 
             /*
              *   We must transmit this energy to each of our other
              *   parents, possibly reduced for traversing our connector.
              *   Calculate the new level after traversing our connector. 
              */
             levelThru = adjustBrightness(ambient, transSensingThru(sense));
 
             /* 
              *   if there's anything left, transmit it to the other
              *   containers 
              */
             if (levelThru >= 2)
             {
                 /* transmit to each container except the source */
                 foreach (local cur in locationList)
                 {
                     /* if this isn't the sender, transmit to it */
                     if (cur != fromParent)
                         cur.shineFromWithin(self, sense, levelThru, fill);
                 }
             }
         }
     }
 
     /*
      *   Build a sense path from a container to me
      */
     sensePathFromWithout(fromParent, sense, trans, obs, fill)
     {
         /* 
          *   if there's better transparency along this path than along any
          *   previous path we've used to visit this item, take this path 
          */
         if (transparencyCompare(trans, tmpTrans_) > 0)
         {
             local transThru;
             
             /* remember the new path to this point */
             tmpTrans_ = trans;
             tmpObstructor_ = obs;
 
             /* we're coming to this object from outside */
             tmpPathIsIn_ = true;
 
             /* transmit to my contents */
             sensePathToContents(sense, trans, obs, fill);
 
             /*
              *   We must transmit this energy to each of our other
              *   parents, possibly reduced for traversing our connector.
              *   Calculate the new level after traversing our connector. 
              */
             transThru = transparencyAdd(trans, transSensingThru(sense));
 
             /* if we changed the transparency, we're the obstructor */
             if (transThru != trans)
                 obs = self;
 
             /* 
              *   if there's anything left, transmit it to the other
              *   containers 
              */
             if (transThru != opaque)
             {
                 /* transmit to each container except the source */
                 foreach (local cur in locationList)
                 {
                     /* if this isn't the sender, transmit to it */
                     if (cur != fromParent)
                         cur.sensePathFromWithin(self, sense,
                                                 transThru, obs, fill);
                 }
             }
         }
     }
 
     /* 
      *   Call a function on each connected container.  Since we provide a
      *   sense path connection among our containers, we must iterate over
      *   each of our containers.  
      */
     forEachConnectedContainer(func, [args])
     {
         forEachContainer(func, args...);
     }
 
     /* 
      *   Return a list of my connected containers.  We connect to all of
      *   our containers, so simply return my location list. 
      */
     getConnectedContainers = (locationList)
 
     /* 
      *   Check moving an object through me.  This is called when we try to
      *   move an object from one of our containers to another of our
      *   containers through me.  By default, we don't allow it.  
      */
     checkMoveThrough(obj, dest)
     {
         /* return an error - cannot move through <self> */
         return new CheckStatusFailure(&cannotMoveThroughMsg, obj, self);
     }
 
     /*
      *   Check touching an object through me.  This is called when an
      *   actor tries to reach from one of my containers through me into
      *   another of my containers.  By default, we don't allow it. 
      */
     checkTouchThrough(obj, dest)
     {
         /* return an error - cannot reach through <self> */
         return new CheckStatusFailure(&cannotReachThroughMsg, dest, self);
     }
 
     /* check for moving via a path */
     checkMoveViaPath(obj, dest, op)
     {
         /* if moving through us, don't allow it */
         if (op == PathThrough)
             return checkMoveThrough(obj, dest);
 
         /* if we can inherit, do so */
         if (canInherit())
             return inherited(obj, dest, op);
 
         /* return success by default */
         return checkStatusSuccess;
     }
 
     /* check for touching via a path */
     checkTouchViaPath(obj, dest, op)
     {
         /* if moving through us, don't allow it */
         if (op == PathThrough)
             return checkTouchThrough(obj, dest);
 
         /* if we can inherit, do so */
         if (canInherit())
             return inherited(obj, dest, op);
 
         /* return success by default */
         return checkStatusSuccess;
     }
 ;
 
 /*
  *   Occluder: this is a mix-in class that can be used with multiple
  *   inheritance to combine with other classes (such as SenseConnector, or
  *   Thing subclasses), to create an "occluded view."  This lets you
  *   exclude certain objects from view, and you can make the exclusion vary
  *   according to the point of view.
  *   
  *   This class is useful for situations where the view from one location
  *   to another is partially obstructed.  For example, suppose we have two
  *   rooms, connected by a window between them.  The window is the sense
  *   connector that connects the two top-level locations, and it makes
  *   objects in one room visible from the point of view of the other room.
  *   Suppose that one room contains a bookcase, with its back to the
  *   window.  From the point of view of the other room, we can't see
  *   anything inside the bookcase.  This class allows for such special
  *   situations.
  *   
  *   Note that occlusion rules are applied "globally" within a room - that
  *   is, anything that an Occluder occludes will be removed from view, even
  *   if it's visible from another, non-occluding connector.  Hence,
  *   occlusion always takes precedence over "inclusion" - if an object is
  *   occluded just once, then it won't be in view, no matter how many times
  *   it's added back into view by other connectors.  This comes from the
  *   order in which the occlusion rules are considered.  Occlusion rules
  *   are always run last, and they can't distinguish the connector that
  *   added an object to view.  So, we first run around and collect up
  *   everything that can be seen, by considering all of the different paths
  *   to seeing those things.  Then, we go through all of the occlusion
  *   rules that apply to the room, and we remove from view everything that
  *   the occluding connectors want to occlude.  
  */
 class Occluder: object
     /*
      *   Do we occlude the given object, in the given sense and from the
      *   given point of view?  This returns true if the object is occluded,
      *   nil if not.  By default, we simply ask the object whether it's
      *   occluded by this occluder from the given POV.  
      */
     occludeObj(obj, sense, pov)
     {
         /* by default, simply ask the object what it thinks */
         return obj.isOccludedBy(self, sense, pov);
     }
 
     /*
      *   When we initialize for the sense path calculation, register to
      *   receive notification after we've finished building the sense
      *   table.  We'll use the notification to remove any occluded objects
      *   from the sense table.  
      */
     clearSenseInfo()
     {
         /* do the normal work */
         inherited();
 
         /* register for notification after we've built the table */
         senseTmp.notifyList.append(self);
     }
 
     /*
      *   Receive notification that the sense path calculation is now
      *   finished.  'objs' is a LookupTable containing all of the objects
      *   involved in the sense path calculation (the objects are the keys
      *   in the table).  Each object in the table now has its tmpXxx_
      *   properties set to the sense path data we've calculated for that
      *   object - tmpTrans_ is the transparency to the object, tmpAmbient_
      *   is the ambient light level at the object, and so on.
      *   
      *   Since our job is to occlude certain objects from view, we'll run
      *   through the table and test each object using our occlusion rule.
      *   If we find that we do occlude an object, we'll set its
      *   transparency to 'opaque' to indicate that it cannot be seen.  
      */
     finishSensePath(objs, sense)
     {
         /* get the point of view of the calculation */
         local pov = senseTmp.pointOfView;
             
         /* run through the table, and apply our rule to each object */
         objs.forEachAssoc(new function(key, val)
         {
             /* if this object is occluded, set its path to opaque */
             if (occludeObj(key, sense, pov))
             {
                 /* set this object to opaque */
                 key.tmpTrans_ = key.tmpTransWithin_ = opaque;
 
                 /* we're the obstructor for the object */
                 key.tmpObstructor_ = key.tmpObstructorWithin_ = self;
             }
         });
     }
 ;
 
 /*
  *   DistanceConnector: a special type of SenseConnector that connects its
  *   locations with distance.  This can be used for things like divided
  *   rooms, where a single physical location is modeled with two or more
  *   Room objects - the north and south end of a large cave, for example.
  *   This is also useful for cases where two rooms are separate but open to
  *   one another, such as a balcony overlooking a courtyard.
  *   
  *   Note that this inherits from both SenseConnector and Intangible.
  *   Intangible is included as a base class because each instance will need
  *   to derive from Thing, so that it fits into the normal sense model, but
  *   will virtually never need any other physical presence in the game
  *   world; Intangible fills both of these needs.  
  */
 class DistanceConnector: SenseConnector, Intangible
     /* all senses are connected through us, but at a distance */
     transSensingThru(sense) { return distant; }
 
     /* 
      *   When checking for reaching through this connector, specialize the
      *   failure message to indicate that distance is the specific problem.
      *   (Without this specialization, we'd get a generic message when
      *   trying to reach through the connector, such as "you can't reach
      *   that through <self>."  
      */
     checkTouchThrough(obj, dest)
     {
         /* we can't touch through this connector due to the distance */
         return new CheckStatusFailure(&tooDistantMsg, dest);
     }
 
     /* 
      *   If we're responsible for blocking a thrown object's flight, we
      *   need to provide a custom message.  The default says that we
      *   deflected the object as though we were a physical barrier.
      *   Instead, in our case, the problem isn't deflection but simply
      *   range - so we need to explain that the projectile fell short of
      *   the target.
      *   
      *   By default, we assume that this connector interposes such a great
      *   distance that a character can't throw something all the way to the
      *   other side, which is why we provide this special message.  If you
      *   do want to allow actors to throw things all the way through the
      *   distance connector to the connected location, you can override
      *   checkMoveThrough like this:
      *   
      *   checkMoveThrough(obj, dest) { return checkStatusSuccess; } 
      */
     throwTargetHitWith(projectile, path)
     {
         /* 
          *   figure out where we fall to when we hit this object, then send
          *   the object being thrown to that location 
          */
         getHitFallDestination(projectile, path)
             .receiveDrop(projectile, new DropTypeShortThrow(self, path));
     }
 ;
 
 /*
  *   A drop-type descriptor for a "short throw," which occurs when the
  *   target is too far away to reach with our throw (i.e., the thrown
  *   object falls short of the target).  
  */
 class DropTypeShortThrow: DropTypeThrow
     construct(target, path)
     {
         /* inherit the default handling */
         inherited(target, path);
 
         /* we care about the *intended* target, not the distance connector */
         target_ = path[path.length()];
     }
 
     standardReport(obj, dest)
     {
         /* show the short-throw report */
         mainReport(&throwFallShortMsg, obj, target_,
                    dest.getNominalDropDestination());
     }
 
     getReportPrefix(obj, dest)
     {
         /* return the short-throw prefix */
         return gActor.getActionMessageObj().throwShortMsg(obj, target_);
     }
 ;
 
TADS 3 Library Manual
Generated on 9/8/2006 from TADS version 3.0.11