objects.t

documentation
 #charset "us-ascii"
 
 /* 
  *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved. 
  *.  Portions based on work by Kevin Forchione, used by permission.  
  *   
  *   TADS 3 Library - objects
  *   
  *   This module defines the basic physical simulation objects (apart from
  *   Thing, the base class for most game objects, which is so large that
  *   it's defined in its own separate module for convenience).  We define
  *   such basic classes as containers, surfaces, fixed-in-place objects,
  *   openables, and lockables.  
  */
 
 /* include the library header */
 #include "adv3.h"
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   LocateInParent - this is a mix-in superclass that defines the location
  *   of the object as the object's lexical parent.  This is useful for
  *   nested object definitions where the next object should be located
  *   within the enclosing object.
  *   
  *   When this class is mixed with Thing or its subclasses, LocateInParent
  *   should go first, so that the location we define here takes precedence.
  */
 LocateInParent: object
     location = (lexicalParent)
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Intangible - this is an object that represents something that can be
  *   sensed but which has no tangible existence, such as a ray of light, a
  *   sound, or an odor. 
  */
 class Intangible: Thing
     /*
      *   The base intangible object has no presence in any sense,
      *   including sight.  Subclasses should override these as appropriate
      *   for the senses in which the object can be sensed.  
      */
     sightPresence = nil
     soundPresence = nil
     smellPresence = nil
     touchPresence = nil
 
     /* intangibles aren't included in regular room/inventory/contents lists */
     isListed = nil
     isListedInInventory = nil
     isListedInContents = nil
 
     /* hide intangibles from 'all' for all actions by default */
     hideFromAll(action) { return true; }
 
     /* don't hide from defaults, though */
     hideFromDefault(action) { return nil; }
 
     /*
      *   Essentially all verbs are meaningless on intangibles.  Each
      *   subclass should re-enable verbs that are meaningful for that
      *   specific type of intangible; to re-enable an action, just define
      *   a verify() handler for the action.
      *   
      *   Note that the verbs we handle via the Default handlers have no
      *   preconditions; since these verbs don't do anything anyway,
      *   there's no need to apply any preconditions to them.  
      */
     dobjFor(Default)
     {
         preCond = []
         verify() { illogical(&notWithIntangibleMsg, self); }
     }
     iobjFor(Default)
     {
         preCond = []
         verify() { illogical(&notWithIntangibleMsg, self); }
     }
 ;
 
 /*
  *   A "vaporous" object is a visible but intangible object: something
  *   visible, and possibly with an odor and a sound, but not something that
  *   can be touched or otherwise physically manipulated.  Fire, smoke, and
  *   fog are examples of this kind of object.  
  */
 class Vaporous: Intangible
     /* we have a sight presence */
     sightPresence = true
 
     /* 
      *   EXAMINE ALL, LISTEN TO ALL, and SMELL ALL apply to us, but hide
      *   from ALL for other actions, as not much else makes sense on us 
      */
     hideFromAll(action)
     {
         return !(action.ofKind(ExamineAction)
                  || action.ofKind(ListenToAction)
                  || action.ofKind(SmellAction));
     }
 
     /* 
      *   We can examine, smell, and listen to these objects, as normal for
      *   any Thing.  To make these verbs work as normal for Thing, we need
      *   to explicitly override the corresponding verifiers, so that we
      *   bypass the dobjFor(Default) verifier in Intangible.  We don't need
      *   to do anything special in the overrides, so just inherit the
      *   default handling; what's important is that we do override the
      *   methods at all. 
      */
     dobjFor(Examine) { verify() { inherited(); } }
     dobjFor(Smell) { verify() { inherited(); } }
     dobjFor(ListenTo) { verify() { inherited(); } }
 
     /* 
      *   look in, look through, look behind, look under, search: since
      *   vaporous objects are usually essentially transparent, these
      *   commands reveal nothing interesting 
      */
     lookInDesc { mainReport(&lookInVaporousMsg, self); }
 
     /* 
      *   downgrade the likelihood of these slightly, and map everything to
      *   LOOK IN 
      */
     dobjFor(LookIn) { verify() { logicalRank(70, 'look in vaporous'); } }
     dobjFor(LookThrough) asDobjFor(LookIn)
     dobjFor(LookBehind) asDobjFor(LookIn)
     dobjFor(LookUnder) asDobjFor(LookIn)
     dobjFor(Search) asDobjFor(LookIn)
 
     /* the message we display for commands we disallow */
     notWithIntangibleMsg = &notWithVaporousMsg
 ;
 
 
 /*
  *   A sensory emanation.  This is an intangible object that represents a
  *   sound, odor, or the like. 
  */
 class SensoryEmanation: Intangible
     /*
      *   Are we currently emanating our sensory information?  This can be
      *   used as an on/off switch to control when we're active.  
      */
     isEmanating = true
     
     /* 
      *   The description shown when the *source* is examined (with "listen
      *   to", "smell", or whatever verb is appropriate to the type of sense
      *   the subclass involves).  This will also be appended to the regular
      *   "examine" description, if we're not marked as ambient.  
      */
     sourceDesc = ""
 
     /* our description, with and without being able to see the source */
     descWithSource = ""
     descWithoutSource = ""
 
     /* 
      *   Our "I am here" message, with and without being able to see the
      *   source.  These are displayed in room descriptions, inventory
      *   descriptions, and by the daemon that schedules background
      *   messages for sensory emanations.
      *   
      *   If different messages are desired as the emanation is mentioned
      *   repeatedly while the emanation remains continuously within sense
      *   range of the player character ("A phone is ringing", "The phone
      *   is still ringing", etc), you can do one of two things.  The
      *   easier way is to use a Script object; each time we need to show a
      *   message, we'll invoke the script.  The other way, which is more
      *   manual but gives you greater control, is to write a method that
      *   checks the displayCount property of self to determine which
      *   iteration of the message is being shown.  displayCount is set to
      *   1 the first time a message is displayed for the object when the
      *   object can first be sensed, and is incremented each we invoke one
      *   of these display routines.  Note that displayCount resets to 1
      *   when the object comes back into sense range after leaving.
      *   
      *   The manual way (writing a method that checks the displayCount)
      *   might be desirable if you want the emanation to fade into the
      *   background gradually as the player character stays in the same
      *   location repeatedly.  This mimics human perception: we notice a
      *   noise or odor most when we first hear it, but if it continues for
      *   an extended period without changing, we'll eventually stop
      *   noticing it.  
      */
     hereWithSource = ""
     hereWithoutSource = ""
 
     /* 
      *   A message to display when the emanation ceases to be within sense
      *   range.  In most cases, this displays nothing at all, but some
      *   emanations might want to note explicitly when the noise/etc
      *   stops.
      */
     noLongerHere = ""
 
     /*
      *   Flag: I'm an "ambient" emanation.  This means we essentially are
      *   part of the background, and are not worth mentioning in our own
      *   right.  If this is set to true, then we won't mention this
      *   emanation at all when it first becomes reachable in its sense.
      *   This should be used for background noises and the like: we won't
      *   ever make an unsolicited mention of them, but they'll still show
      *   up in explicit 'listen' commands and so on.  
      */
     isAmbient = nil
 
     /*
      *   The schedule for displaying messages about the emanation.  This
      *   is a list of intervals between messages, in game clock times.
      *   When the player character can repeatedly sense this emanation for
      *   multiple consecutive turns, we'll use this schedule to display
      *   messages periodically about the noise/odor/etc.
      *   
      *   Human sensory perception tends to be "edge-sensitive," which
      *   means that we tend to perceive sensory input most acutely when
      *   something changes.  When a sound or odor is continually present
      *   without variation for an extended period, it tends to fade into
      *   the background of our awareness, so that even though it remains
      *   audible, we gradually stop noticing it.  This message display
      *   schedule mechanism is meant to approximate this perceptual model
      *   by allowing the sensory emanation to specify how noticeable the
      *   emanation remains during continuous exposure.  Typically, a
      *   continuous emanation would have relatively frequent messages
      *   (every two turns, say) for a couple of iterations, then would
      *   switch to infrequent messages.  Emanations that are analogous to
      *   white noise would probably not be mentioned at all after the
      *   first couple of messages, because the human senses are especially
      *   given to treating such input as background.
      *   
      *   We use this list by applying each interval in the list once and
      *   then moving to the next entry in the list.  The first entry in
      *   the list is the interval between first sensing the emanation and
      *   displaying the first "still here" message.  When we reach the end
      *   of the list, we simply repeat the last interval in the list
      *   indefinitely.  If the last entry in the list is nil, though, we
      *   simply never produce another message.  
      */
     displaySchedule = [nil]
 
     /*
      *   Show our "I am here" description.  This is the description shown
      *   as part of our room's description.  We show our hereWithSource or
      *   hereWithoutSource message, according to whether or not we can see
      *   the source object.  
      */
     emanationHereDesc()
     {
         local actor;
         local prop;
 
         /* if we're not currently emanating, there's nothing to do */
         if (!isEmanating)
             return;
         
         /* note that we're mentioning the emanation */
         noteDisplay();
 
         /* 
          *   get the actor driving the description - if there's a command
          *   active, use the command's actor; otherwise use the player
          *   character
          */
         if ((actor = gActor) == nil)
             actor = gPlayerChar;
 
         /* our display varies according to our source's visibility */
         prop = (canSeeSource(actor) ? &hereWithSource : &hereWithoutSource);
 
         /* 
          *   if it's a Script object, invoke the script; otherwise, just
          *   invoke the property 
          */
         if (propType(prop) == TypeObject && self.(prop).ofKind(Script))
             self.(prop).doScript();
         else
             self.(prop);
     }
 
     /*
      *   Show a message describing that we cannot see the source of this
      *   emanation because the given obstructor is in the way.  This
      *   should be overridden for each subclass. 
      */
     cannotSeeSource(obs) { }
 
     /* 
      *   Get the source of the noise/odor/whatever, as perceived by the
      *   current actor.  This is the object we appear to be coming from.
      *   By default, an emanation is generated by its direct container,
      *   and by default this is apparent to actors, so we'll simply return
      *   our direct container.
      *   
      *   If the source is not apparent, this should simply return nil.  
      */
     getSource() { return location; }
 
     /* determine if our source is apparent and visible */
     canSeeSource(actor)
     {
         local src;
         
         /* get our source */
         src = getSource();
 
         /* 
          *   return true if we have an apparent source, and the apparent
          *   source is visible to the current actor 
          */
         return src != nil && actor.canSee(src);
     }
 
     /*
      *   Note that we're displaying a message about the emanation.  This
      *   method should be called any time a message about the emanation is
      *   displayed, either by an explicit action or by our background
      *   daemon.
      *   
      *   We'll adjust our next display time so that we wait the full
      *   interval at the current point in the display schedule before we
      *   show any background message about this object.  Note we do not
      *   advance through the schedule list; instead, we merely delay any
      *   further message by the interval at the current point in the
      *   schedule list.  
      */
     noteDisplay()
     {
         /* calculate our next display time */
         calcNextDisplayTime();
 
         /* count the display */
         if (displayCount == nil)
             displayCount = 1;
         else
             ++displayCount;
     }
 
     /*
      *   Note an indirect message about the emanation.  This can be used
      *   when we don't actually display a message ourselves, but another
      *   object (usually our source object) describes the emanation; for
      *   example, if our source object mentions the noise it's making when
      *   it is examined, it should call this method to let us know we have
      *   been described indirectly.  This method advances our next display
      *   time, just as noteDisplay() does, but this method doesn't count
      *   the display as a direct display. 
      */
     noteIndirectDisplay()
     {
         /* calculate our next display time */
         calcNextDisplayTime();
     }
     
     /*
      *   Begin the emanation.  This is called from the sense change daemon
      *   when the item first becomes noticeable to the player character -
      *   for example, when the player character first enters the room
      *   containing the emanation, or when the emanation is first
      *   activated.  
      */
     startEmanation()
     {
         /* if we're an ambient emanation only, don't mention it */
         if (isAmbient)
             return;
 
         /* 
          *   if we've already initialized our scheduling, we must have
          *   been explicitly mentioned, such as by a room description - in
          *   this case, act as though we're continuing our emanation 
          */
         if (scheduleIndex != nil)
         {
             continueEmanation();
             return;
         }
 
         /* show our message */
         emanationHereDesc;
     }
 
     /*
      *   Continue the emanation.  This is called on each turn in which the
      *   emanation remains continuously within sense range of the player
      *   character.  
      */
     continueEmanation()
     {
         /* 
          *   if we are not to run again, our next display time will be set
          *   to zero - do nothing in this case 
          */
         if (nextDisplayTime == 0 || nextDisplayTime == nil)
             return;
 
         /* if we haven't yet reached our next display time, do nothing */
         if (Schedulable.gameClockTime < nextDisplayTime)
             return;
 
         /* 
          *   Advance to the next schedule interval, if we have one.  If
          *   we're already on the last schedule entry, simply repeat it
          *   forever. 
          */
         if (scheduleIndex < displaySchedule.length())
             ++scheduleIndex;
 
         /* show our description */
         emanationHereDesc;
     }
 
     /*
      *   End the emanation.  This is called when the player character can
      *   no longer sense the emanation. 
      */
     endEmanation()
     {
         /* show our "no longer here" message */
         noLongerHere;
 
         /* uninitialize the display scheduling */
         scheduleIndex = nil;
         nextDisplayTime = nil;
     }
 
     /*
      *   Calculate our next display time.  The caller must set our
      *   scheduleIndex to the correct index prior to calling this.  
      */
     calcNextDisplayTime()
     {
         local delta;
 
         /* if our scheduling isn't initialized, set it up now */
         if (scheduleIndex == nil)
         {
             /* start at the first display schedule interval */
             scheduleIndex = 1;
         }
         
         /* get the next display interval from the schedule list */
         delta = displaySchedule[scheduleIndex];
 
         /* 
          *   if the current display interval is nil, it means that we're
          *   never to display another message 
          */
         if (delta == nil)
         {
             /* 
              *   we're not to display again - simply set the next display
              *   time to zero and return 
              */
             nextDisplayTime = 0;
             return;
         }
 
         /* 
          *   our next display time is the current game clock time plus the
          *   interval 
          */
         nextDisplayTime = Schedulable.gameClockTime + delta;
     }
 
     /*
      *   Internal counters that keep track of our display scheduling.
      *   scheduleIndex is the index in the displaySchedule list of the
      *   interval we're waiting to expire; nextDisplayTime is the game
      *   clock time of our next display.  noiseList and odorList are lists
      *   of senseInfo entries for the sound and smell senses,
      *   respectively, indicating which objects were within sense range on
      *   the last turn.  displayCount is the number of times in a row
      *   we've displayed a message already.  
      */
     scheduleIndex = nil
     nextDisplayTime = nil
     noiseList = nil
     odorList = nil
     displayCount = nil
 
     /*
      *   Class method implementing the sensory change daemon.  This runs
      *   on each turn to check for changes in the set of objects the
      *   player can hear and smell, and to generate "still here" messages
      *   for objects continuously within sense range for multiple turns.  
      */
     noteSenseChanges()
     {
         /* emanations don't change anything, so turn on caching */
         libGlobal.enableSenseCache();
 
         /* note sound changes */
         noteSenseChangesFor(sound, &noiseList, Noise);
 
         /* note odor changes */
         noteSenseChangesFor(smell, &odorList, Odor);
 
         /* done with sense caching */
         libGlobal.disableSenseCache();
     }
 
     /*
      *   Note sense changes for a particular sense.  'listProp' is the
      *   property of SensoryEmanation giving the list of SenseInfo entries
      *   for the sense on the previous turn.  'sub' is a subclass of ours
      *   (such as Noise) giving the type of sensory emanation used for
      *   this sense. 
      */
     noteSenseChangesFor(sense, listProp, sub)
     {
         local newInfo;
         local oldInfo;
 
         /* get the old table of SenseInfo entries for the sense */
         oldInfo = self.(listProp);
 
         /* 
          *   Get the new table of items we can reach in the given sense,
          *   and reduce it to include only emanations of the subclass of
          *   interest.  
          */
         newInfo = gPlayerChar.senseInfoTable(sense);
         newInfo.forEachAssoc(new function(obj, info)
         {
             /* 
              *   remove this item if it's not of the subclass of interest,
              *   or if it's not currently emanating 
              */
             if (!obj.ofKind(sub) || !obj.isEmanating)
                 newInfo.removeElement(obj);
         });
 
         /* run through the new list and note each change */
         newInfo.forEachAssoc(new function(obj, info)
         {
             /* treat this as a new command visually */
             "<.commandsep>";
         
             /* 
              *   Check to see whether the item is starting anew or was
              *   already here on the last turn.  If the item was in our
              *   list from the previous turn, it was already here.  
              */
             if (oldInfo == nil || oldInfo[obj] == nil)
             {
                 /* 
                  *   the item wasn't in sense range on the last turn, so
                  *   it is becoming newly noticeable 
                  */
                 obj.startEmanation();
             }
             else
             {
                 /* the item was already here - continue its emanation */
                 obj.continueEmanation();
             }
         });
 
         /* run through the old list and note each item no longer sensed */
         if (oldInfo != nil)
         {
             oldInfo.forEachAssoc(new function(obj, info)
             {
                 /* if this item isn't in the new list, note its departure */
                 if (newInfo[obj] == nil)
                 {
                     /* treat this as a new command visually */
                     "<.commandsep>";
                     
                     /* note the departure */
                     obj.endEmanation();
                 }
             });
         }
 
         /* store the current list for comparison the next time we run */
         self.(listProp) = newInfo;
     }
 
     /* 
      *   Examine the sensory emanation.  We'll show our descWithSource or
      *   descWithoutSource, according to whether or not we can see the
      *   source object. 
      */
     dobjFor(Examine)
     {
         verify() { inherited(); }
         action()
         {
             /* note that we're displaying a message about us */
             noteDisplay();
             
             /* display our sound description */
             if (canSeeSource(gActor))
             {
                 /* we can see the source */
                 descWithSource;
             }            
             else
             {
                 local src;
             
                 /* show the unseen-source version of the description */
                 descWithoutSource;
 
                 /* 
                  *   If we have a source, find out what's keeping us from
                  *   seeing the source; in other words, find the opaque
                  *   visual obstructor on the sense path to the source.  
                  */
                 if ((src = getSource()) != nil)
                 {
                     local obs;
                     
                     /* get the visual obstructor */
                     obs = gActor.findVisualObstructor(src);
                     
                     /* 
                      *   If we found an obstructor, and we can see it, add
                      *   a message describing the obstruction.  If we
                      *   can't see the obstructor, we can't localize the
                      *   sensory emanation at all.  
                      */
                     if (obs != nil && gActor.canSee(obs))
                         cannotSeeSource(obs);
                 }
             }
         }
     }
 ;
 
 /*
  *   Noise - this is an intangible object representing a sound.
  *   
  *   A Noise object is generally placed directly within the object that is
  *   generating the noise.  This will ensure that the noise is
  *   automatically in scope whenever the object is in scope (or, more
  *   precisely, whenever the object's contents are in scope) and with the
  *   same sense attributes.
  *   
  *   By default, when a noise is specifically examined via "listen to",
  *   and the container is visible, we'll mention that the noise is coming
  *   from the container.
  */
 class Noise: SensoryEmanation
     /* 
      *   by default, we have a definite presence in the sound sense if
      *   we're emanating our noise
      */
     soundPresence = (isEmanating)
 
     /* 
      *   By default, a noise is listed in a room description (i.e., on LOOK
      *   or entry to a room) unless it's an ambient background noise..  Set
      *   this to nil to omit the noise from the room description, while
      *   still allowing it to be heard in an explicit LISTEN command.  
      */
     isSoundListedInRoom = (!isAmbient && isEmanating)
 
     /* show our description as part of a room description */
     soundHereDesc() { emanationHereDesc(); }
 
     /* explain that we can't see the source because of the obstructor */
     cannotSeeSource(obs) { obs.cannotSeeSoundSource(self); }
 
     /* treat "listen to" the same as "examine" */
     dobjFor(ListenTo) asDobjFor(Examine)
 
     /* "examine" requires that the object is audible */
     dobjFor(Examine)
     {
         preCond = [objAudible]
     }
 ;
 
 /*
  *   Odor - this is an intangible object representing an odor. 
  */
 class Odor: SensoryEmanation
     /* 
      *   by default, we have a definite presence in the smell sense if
      *   we're currently emanating our odor 
      */
     smellPresence = (isEmanating)
 
     /* 
      *   By default, an odor is listed in a room description (i.e., on LOOK
      *   or entry to a room) unless it's an ambient background odor.  Set
      *   this to nil to omit the odor from the room description, while
      *   still allowing it to be listed in an explicit SMELL command.  
      */
     isSmellListedInRoom = (!isAmbient && isEmanating)
 
     /* mention the odor as part of a room description */
     smellHereDesc() { emanationHereDesc(); }
 
     /* explain that we can't see the source because of the obstructor */
     cannotSeeSource(obs) { obs.cannotSeeSmellSource(self); }
 
     /* handle "smell" using our "examine" handler */
     dobjFor(Smell) asDobjFor(Examine)
 
     /* "examine" requires that the object is smellable */
     dobjFor(Examine)
     {
         preCond = [objSmellable]
     }
 ;
 
 /*
  *   SimpleNoise is for cases where a noise is an ongoing part of a
  *   location, so (1) it's not necessary to distinguish source and
  *   sourceless versions of the description, and (2) there are no
  *   scheduled reports for the noise.  For these cases, all of the
  *   messages default to the basic 'desc' property.  Note that we make
  *   this type of noise "ambient" by default, which means that we won't
  *   automatically include it in room descriptions.  
  */
 class SimpleNoise: Noise
     isAmbient = true
     sourceDesc { desc; }
     descWithSource { desc; }
     descWithoutSource { desc; }
     hereWithSource { desc; }
     hereWithoutSource { desc; }
 ;
 
 /* SimpleOdor is the olfactory equivalent of SimpleNoise */
 class SimpleOdor: Odor
     isAmbient = true
     sourceDesc { desc; }
     descWithSource { desc; }
     descWithoutSource { desc; }
     hereWithSource { desc; }
     hereWithoutSource { desc; }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Sensory Event.  This is an object representing a transient event,
  *   such as a sound, visual display, or odor, to which some objects
  *   observing the event might react.
  *   
  *   A sensory event differs from a sensory emanation in that an emanation
  *   is ongoing and passive, while an event is isolated in time and
  *   actively notifies observers.  
  */
 class SensoryEvent: object
     /* 
      *   Trigger the event.  This routine must be called at the time when
      *   the event is to occur.  We'll notify every interested observer
      *   capable of sensing the event that the event is occurring, so
      *   observers can take appropriate action in response to the event.
      *   
      *   'source' is the source object - this is the physical object in
      *   the simulation that is causing the event.  For example, if the
      *   event is the sound of a phone ringing, the phone would probably
      *   be the source object.  The source is used to determine which
      *   observers are capable of detecting the event: an observer must be
      *   able to sense the source object in the appropriate sense to be
      *   notified of the event.  
      */
     triggerEvent(source)
     {
         /* 
          *   Run through all objects connected to the source object by
          *   containment, and notify any that are interested and can
          *   detect the event.  Containment is the only way sense
          *   information can propagate, so we can limit our search
          *   accordingly.
          *   
          *   Connection by containment is no guarantee of a sense
          *   connection: it's a necessary, but not sufficient, condition.
          *   Because it's a necessary condition, though, we can use it to
          *   limit the number of objects we have to test with a more
          *   expensive sense path calculation.  
          */
         source.connectionTable().forEachAssoc(new function(cur, val)
         {
             /* 
              *   If this object defines the observer notification method,
              *   then it might be interested in the event.  If the object
              *   doesn't define this method, then there's no way it could
              *   be interested.  (We make this test before checking the
              *   sense path because checking to see if an object defines a
              *   property is fast and simple, while the sense path
              *   calculation could be expensive.) 
              */
             if (cur.propDefined(notifyProp, PropDefAny))
             {
                 local info;
                 
                 /* 
                  *   This object might be interested in the event, so
                  *   check to see if the object can sense the event.  If
                  *   this object can sense the source object at all (i.e.,
                  *   the sense path isn't 'opaque'), then notify the
                  *   object of the event.  
                  */
                 info = cur.senseObj(sense, source);
                 if (info.trans != opaque)
                 {
                     /* 
                      *   this observer object can sense the source of the
                      *   event, so notify it of the event 
                      */
                     cur.(notifyProp)(self, source, info);
                 }
             }
         });
     }
 
     /* the sense in which the event is observable */
     sense = nil
 
     /* 
      *   the notification property - this is the property we'll invoke on
      *   each observer to notify it of the event 
      */
     notifyProp = nil
 ;
 
 /*
  *   Visual event 
  */
 class SightEvent: SensoryEvent
     sense = sight
     notifyProp = &notifySightEvent
 ;
 
 /* 
  *   Visual event observer.  This is a mix-in that can be added to any
  *   other classes.  
  */
 class SightObserver: object
     /*
      *   Receive notification of a sight event.  This routine is called
      *   whenever a SightEvent occurs within view of this object.
      *   
      *   'event' is the SightEvent object; 'source' is the physical
      *   simulation object that is making the visual display; and 'info'
      *   is a SenseInfo object describing the viewing conditions from this
      *   object to the source object.  
      */
     notifySightEvent(event, source, info) { }
 ;
 
 /*
  *   Sound event 
  */
 class SoundEvent: SensoryEvent
     sense = sound
     notifyProp = &notifySoundEvent
 ;
 
 /* 
  *   Sound event observer.  This is a mix-in that can be added to any
  *   other classes.  
  */
 class SoundObserver: object
     /*
      *   Receive notification of a sound event.  This routine is called
      *   whenever a SoundEvent occurs within hearing range of this object.
      */
     notifySoundEvent(event, source, info) { }
 ;
 
 /*
  *   Smell event 
  */
 class SmellEvent: SensoryEvent
     sense = smell
     notifyProp = &notifySmellEvent
 ;
 
 /* 
  *   Smell event observer.  This is a mix-in that can be added to any
  *   other classes.  
  */
 class SmellObserver: object
     /*
      *   Receive notification of a smell event.  This routine is called
      *   whenever a SmellEvent occurs within smelling range of this
      *   object.  
      */
     notifySmellEvent(event, source, info) { }
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Hidden - this is an object that's present but not visible to any
  *   actors.  The object will simply not be visible in the 'sight' sense
  *   until discovered. 
  */
 class Hidden: Thing
     /* we can't be seen until discovered */
     canBeSensed(sense, trans, ambient)
     {
         /* 
          *   If the sense is sight, and we haven't been discovered yet, we
          *   cannot be sensed.  Otherwise, inherit the normal handling. 
          */
         if (sense == sight && !discovered)
             return nil;
         else
             return inherited(sense, trans, ambient);
     }
 
     /* 
      *   Have we been discovered yet?
      *   
      *   Note that this should be a simple property value, not a method.
      *   It's risky to make this a method because it's evaluated from
      *   within some of the low-level scope/sense calculations, and those
      *   calculations depend upon certain global variables.  If you make
      *   this property into a method, you could indirectly call another
      *   method that changes some of the same globals, which could disrupt
      *   the main scope/sense calculations and cause other, seemingly
      *   unrelated objects to mysteriously appear or disappear at the wrong
      *   times.  If you need to calculate this value dynamically, you could
      *   explicitly assign the property a new value in something like a
      *   daemon or an afterAction() method.
      *   
      *   (The warning above is a bit more conservative than is strictly
      *   necessary.  It actually is safe to make 'discovered' a method,
      *   *provided* that the method doesn't ever call anything that's
      *   involved in the scope/sense calculations.  For example, never call
      *   methods like senseObj(), senseAmbientMax(), or
      *   sensePresenceList(), or anything that calls those.  In most cases,
      *   it's safe to call non-sense-related methods, like isOpen() or
      *   isIn().)  
      */
     discovered = nil
 
     /* mark the object as discovered */
     discover()
     {
         local pc;
         
         /* note that we've been discovered */
         discovered = true;
 
         /* mark me and my contents as having been seen */
         if ((pc = gPlayerChar).canSee(self))
         {
             /* mark me as seen */
             pc.setHasSeen(self);
 
             /* mark my visible contents as see */
             setContentsSeenBy(pc.visibleInfoTable(), pc);
         }
     }
 ;
  
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Collective - this is an object that can be used to refer to a group of
  *   other (usually equivalent) objects collectively.  In most cases, this
  *   object will be a separate game object that contains or can contain the
  *   individuals: a bag of marbles can be a collective for the marbles, or
  *   a book of matches can be a collective for the matchsticks.
  *   
  *   A collective object is usually given the same plural vocabulary as its
  *   individuals.  When we use that plural vocabulary, we will filter for
  *   or against the collective, as determined by the noun phrase
  *   production, when the player uses the collective term.
  *   
  *   This is a mix-in class, intended to be used along with other (usually
  *   Thing-derived) superclasses.  
  */
 class Collective: object
     filterResolveList(lst, action, whichObj, np, requiredNum)
     {
         /* scan for my matching individuals */
         foreach (local cur in lst)
         {
             /* if this one's a matching individual, decide what to do */
             if (isCollectiveFor(cur.obj_))
             {
                 /* 
                  *   We're a collective for this object.  If the noun
                  *   phrase production wants us to filter for collectives,
                  *   remove the individual and keep me (the collective);
                  *   otherwise, keep the individual and remove me. 
                  */
                 if (np.filterForCollectives)
                 {
                     /* 
                      *   we want to keep the collective, so remove this
                      *   individual item 
                      */
                     lst -= cur;
                 }
                 else
                 {
                     /* 
                      *   we want to keep individuals, so remove the
                      *   collective (i.e., myself) 
                      */
                     lst -= lst.valWhich({x: x.obj_ == self});
 
                     /* 
                      *   we can only be in the list once, so there's no
                      *   need to keep looking - if we found another item
                      *   for which we're a collective, all we'd do is try
                      *   to remove myself again, which would be pointless
                      *   since I'm already gone 
                      */
                     break;
                 }
             }
         }
 
         /* return the result */
         return lst;
     }
 
     /*
      *   Determine if I'm a collective object for the given object.
      *   
      *   In order to be a collective for some objects, an object must have
      *   vocubulary for the plural name, and must return true from this
      *   method for the collected objects.  
      */
     isCollectiveFor(obj) { return nil; }
 ;
 
 /*
  *   A "collective group" object.  This is an abstract object: the player
  *   doesn't think of this as a physically separate object, but rather as a
  *   collection of a bunch of individual objects.  For example, if you had
  *   a group of floor-number buttons in an elevator, you might create a
  *   CollectiveGroup to represent the buttons as a collection - from the
  *   player's perspective, there's not a separate physical object called
  *   "the buttons," but it might nonetheless be handy to refer to "the
  *   buttons" collectively as a single entity in commands.  CollectiveGroup
  *   is designed for such situations.
  *   
  *   There are two ways to use CollectiveGroup: as a non-physical,
  *   non-simulation object whose only purpose is to field a few specific
  *   commands; or as a physical simulation object that shows up separately
  *   as an object in its own right.
  *   
  *   First: you can use a CollectiveGroup as a non-physical object, which
  *   essentially means it has a nil 'location'.  The group object doesn't
  *   actually appear in any location.  Instead, it'll be brought into the
  *   sensory system automatically by its individuals, and it'll have the
  *   same effective sensory status as the most visible/audible/etc of its
  *   individuals.  This choice is appropriate when the individuals are
  *   mobile, so they might be scattered around the game map, hence the
  *   group object might need to be invoked anywhere.  With this option, you
  *   normally won't want to make the CollectiveGroup handle very many
  *   commands, because you'll have to completely customize each command you
  *   want it to handle, in order to properly account for the possible
  *   scattering of the individuals.  For example, if you want the group
  *   object to handle the TAKE command, you'll have to figure out which
  *   individuals are in reach, and specially program the procedure for
  *   taking each of the available individuals.
  *   
  *   Second: you can use CollectiveGroup as a simulation object, and
  *   actually set its 'location' to the location of its individuals.  The
  *   group object in this case shows up in the simulation alongside its
  *   individuals.  This is a good choice if the individuals are fixed in
  *   place, all in one place, because you can simply put the group object
  *   in the same location as the individuals without worrying that the
  *   individuals will move around the game later on.  This is much easier
  *   to handle than the first case above, mostly because commands that
  *   physically manipulate the individuals (such as TAKE) aren't a factor.
  *   In this set-up, you can easily let the group object handle many
  *   actions, since it won't have to do much apart from showing the default
  *   failure messages that a Fixed would generate in any other situation.
  *   
  *   The parser will substitute a CollectiveGroup object for its
  *   individuals when (1) any of the individuals are in scope, (2) the
  *   CollectiveGroup has vocabulary that matches a noun phrase in the
  *   player's input, and (3) the conditions for substitution, defined by
  *   isCollectiveQuant and isCollectiveAction, are met.
  *   
  *   (The substitution itself is handled in two steps.  First, an
  *   individual will add the group object to the sense connection list
  *   whenever the individual is in the connection list, which will bring
  *   the object into scope, so the parser will be able to match the
  *   vocabulary from the group object any time an individual is in scope.
  *   Once the group object is matched, its filterResolveList method will
  *   throw out either the group object or all of the individuals, depending
  *   on whether or not the isCollectiveQuant and isCollectiveAction tests
  *   are met.)
  *   
  *   For example, we might have a bunch of coins and paper bills in a game,
  *   and give them all a plural word 'money'.  We then also create a
  *   collective group object with plural word 'money'.  We set the
  *   collectiveGroup property of each coin and bill object to refer to the
  *   collective group object.  Whenever the player uses 'money' in a
  *   command, the individual coins and bills will initially match, and the
  *   group object will also match.  The group object will then either throw
  *   itself out, keeping only the individuals, or will throw out the
  *   individuals.  If the group object decides to field the command, it
  *   will be the only matching object, so a command like "examine money"
  *   will be directed to the single collective group object, rather than
  *   directed to the matching individuals one at a time.  This allows the
  *   game to present simpler, more elegant responses to commands on the
  *   individuals as a group.
  *   
  *   By default, the only action we handle is Examine.  Each instance must
  *   provide a suitable description so that when the collective is
  *   examined, we describe the group of individuals appropriately.  
  */
 class CollectiveGroup: Thing
     /* collective group objects are usually named in plural terms */
     isPlural = true
 
     /*
      *   Filter a noun phrase resolution list.
      *   
      *   If there are any objects in the resolution list for which we're a
      *   collective, we'll check to see whether we want to the collective
      *   or keep the individuals.  We want to keep the collective if the
      *   action is one we can handle collectively; otherwise, we want to
      *   drop the collective and let the individuals handle the action
      *   instead.
      *   
      *   Note that, when any of our individuals are in scope, we're in
      *   scope.  This means that the collective is always in the
      *   resolution list, along with the individuals, if (1) any
      *   individuals are in scope, and (2) the vocabulary used in the noun
      *   phrase matches the collective object.  If the vocabulary doesn't
      *   match the collective, the parser simply won't include the
      *   collective in the resolution list by virtue of the normal
      *   vocabulary selection mechanism, so we'll never reach this point.
      *   
      *   By default, the collective object will be ignored if a specific
      *   number of objects is required.  When the player explicitly
      *   specifies a quantity (by a phrase like "the five coins" or "both
      *   coins"), we'll assume they want to iterate over individuals
      *   rather than operate on the collection.    
      */
     filterResolveList(lst, action, whichObj, np, requiredNum)
     {
         /*
          *   If we want to use the collective for the current action and
          *   the required quantity, keep the collective; otherwise, if
          *   there are any individuals, keep the individuals and filter
          *   out the collective group.  If there are no matching
          *   individuals, keep the collective group object, since there's
          *   nothing to replace it.  
          */
         if (isCollectiveQuant(np, requiredNum)
             && isCollectiveAction(action, whichObj))
         {
             /* 
              *   We can handle the action collectively, so keep myself, and
              *   get rid of the individuals.  We want to discard the
              *   individuals because we want the entire action to be
              *   handled by the collective object, rather than iterating
              *   over the individuals.  So, discard each object that has
              *   'self' as a collectiveGroup (which is to say, keep each
              *   object that *doesn't* have collectiveGroup 'self').  
              */
             lst = lst.subset({x: !x.obj_.hasCollectiveGroup(self)});
         }
         else if (lst.indexWhich({x: x.obj_.hasCollectiveGroup(self)}) != nil)
         {
             /* 
              *   We can't handle the action collectively, and the list
              *   includes at least one of our individuals, so let the
              *   individuals handle it.  Simply remove myself from the
              *   list.  
              */
             lst = lst.removeElementAt(lst.indexWhich({x: x.obj_ == self}));
         }
 
         /* return the updated list */
         return lst;
     }
 
     /*
      *   "Unfilter" a pronoun antecedent list.  We'll restore the
      *   individuals to the list so that we can choose anew, for the new
      *   command, whether to select the group object or the individuals.
      *   
      *   For example, suppose there's a CollectiveGroup for a set of
      *   elevator buttons that handles the Examine command, but no other
      *   commands.  Now suppose the player types in these commands:
      *   
      *.  >examine buttons
      *.  >push them
      *   
      *   On the first command, the CollectiveGroup object will filter out
      *   the individual buttons in filterResolveList, because the group
      *   object handles the Examine command on behalf of the individuals.
      *   This will set the pronoun antecedent for IT and THEM to the group
      *   object, because that's the program object that handled the
      *   action.  On the second command, if the player had typed simply
      *   PUSH BUTTONS, the collective group object would have filtered
      *   *itself* out, keeping the individuals.  However, the raw pronoun
      *   binding for THEM is the group object; if we did nothing to change
      *   this, we'd get a different response for PUSH THEM than we'd get
      *   for PUSH BUTTONS.  That's where this routine comes in: by
      *   restoring the individuals, we let filterResolveList() make the
      *   decision about what to keep anew for the pronoun.  
      */
     expandPronounList(typ, lst)
     {
         /* restore our individuals to the list */
         forEachInstance(Thing, new function(obj) {
             if (obj.hasCollectiveGroup(self))
                 lst += obj;
         });
 
         /* return the list */
         return lst;
     }
     
     /*
      *   Check the action to determine if it's one that we want to handle
      *   collectively.  If so, return true; if not, return nil. 
      */
     isCollectiveAction(action, whichObj)
     {
         /* we handle 'Examine' */
         if (action.ofKind(ExamineAction))
             return true;
 
         /* it's not one of ours */
         return nil;
     }
 
     /*
      *   Check to see if we're a collective for the given quantity.  By
      *   default, we return true only when no quantity is specified.  
      */
     isCollectiveQuant(np, requiredNum)
     {
         /* if no quantity was specified, use the collective */
         return (requiredNum == nil);
     }
 
     /*
      *   Get a list of the individuals that can be sensed, given the
      *   information table for the desired sense (for visible items, this
      *   can be obtained by calling gActor.visibleInfoTable()).  This is a
      *   service routine that can be useful for purposes such as writing a
      *   description routine for the collective.  For example, a "money"
      *   collective object might want to count up the sum of money visible
      *   and show that.
      *   
      *   Note that it's possible for this to return an empty list.  The
      *   caller can deal with this in a description, for example, by
      *   indicating that the collection cannot be seen.  
      */
     getVisibleIndividuals(tab)
     {
         /* keep only those items that are individuals of this collective */
         tab.forEachAssoc(new function(key, val)
         {
             /* remove this item if it's not an individual of mine */
             if (!key.hasCollectiveGroup(self))
                 tab.removeElement(key);
         });
 
         /* return a list of the objects (i.e., the table's keys) */
         return tab.keysToList();
     }
 
     /*
      *   When we have no location, we're an abstract object without any
      *   physical presence in the game world.  However, we still want to
      *   show up in the senses to the same extent our individuals do.  To
      *   do this, we override this method so that we use the same sense
      *   data as the most visible (or whatever) of our individuals.  
      */
     addToSenseInfoTable(sense, tab)
     {
         /* if we have no location, mimic our best individual */
         if (location == nil && !ofKind(BaseMultiLoc))
         {
             /* check everything in the connection table */
             tab.forEachAssoc(new function(cur, val) {
                 /* if this is one of our individuals, check it */
                 if (cur.hasCollectiveGroup(self))
                 {
                     local t;
                     
                     /* 
                      *   If it's the best or only one so far, adopt its
                      *   sense status.  Consider it the best if it has a
                      *   more transparent transparency than the best so
                      *   far, or its transparency is the same and it has a
                      *   high ambient level.  
                      */
                     t = transparencyCompare(cur.tmpTrans_, tmpTrans_);
                     if (t > 0 || (t == 0 && cur.tmpAmbient_ > tmpAmbient_))
                     {
                         /* it's better than our settings; mimic it */
                         tmpTrans_ = cur.tmpTrans_;
                         tmpAmbient_ = cur.tmpAmbient_;
                         tmpObstructor_ = cur.tmpObstructor_;
                     }
                 }
             });
         }
 
         /* inherit the standard handling */
         inherited(sense, tab);
     }
 
     /*
      *   When we have no location, we want to create our own special
      *   containment path, just as we create our own special SenseInfo.  
      */
     specialPathFrom(src, vec)
     {
         /* if we have a location, use the normal handling */
         if (location != nil || ofKind(BaseMultiLoc))
             inherited(src, vec);
 
         /* look for an individual among the source object's connections */
         src.connectionTable().forEachAssoc(new function(cur, val) {
             /* if this is one of our individuals, check it */
             if (cur.hasCollectiveGroup(self))
             {
                 /* add this individual's paths to the vector */
                 vec.appendAll(src.getAllPathsTo(cur));
             }
         });
     }
 
     /*
      *   CollectiveGroup objects are not normally listable in any
      *   situations.  Since a collective group is merely a parser stand-in
      *   for its individuals, we don't want it to appear as a separate
      *   object in the game. 
      */
     isListedInContents = nil
     isListedInInventory = nil
 ;
 
 /*
  *   An "itemizing" collective group is like a regular collective group,
  *   but the Examine action itemizes the individual visible items making up
  *   the group.  We itemize the individuals instead of showing the 'desc'
  *   for the overall group object, as the basic collective group class
  *   does.  
  */
 class ItemizingCollectiveGroup: CollectiveGroup
     /*
      *   Override the main Examine handling.  By default, we'll list the
      *   individuals that are visible, and separately list those that are
      *   being carried by the actor.  If none of our individuals are
      *   visible, simply say so.
      */
     mainExamine()
     {
         local info;
         local vis;
         local carried, here;
 
         /* get the visible info table */
         info = gActor.visibleInfoTable();
         
         /* get the list of visible individuals */
         vis = getVisibleIndividuals(info);
 
         /* if any individuals are visible, list them */
         if (vis.length() != 0)
         {
             /* separate out the individuals being carried */
             carried = vis.subset({x: x.isIn(gActor)});
             here = vis - carried;
 
             /* show the items that are here but not being carried, if any */
             if (here.length() != 0)
                 gActor.location.roomContentsLister.showList(
                     gActor, nil, here, 0, 0, info, nil);
 
             /* separately, show the items being carried, if any */
             if (carried.length() != 0)
                 gActor.inventoryLister.showList(
                     gActor, gActor, carried, 0, 0, info, nil);
         }
         else
         {
             /* 
              *   None are visible.  If it's dark in the location, simply
              *   say so; otherwise, say that we can't see any of me. 
              */
             if (!gActor.isLocationLit())
                 reportFailure(&tooDarkMsg);
             else
                 reportFailure(&mustBeVisibleMsg, self);
         }
     }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   A readable object.  Any ordinary object will show its normal full
  *   description when read, but an object that is explicitly readable will
  *   have elevated logicalness for the "read" action, and can optionally
  *   show a separate description when read. 
  */
 class Readable: Thing
     /* 
      *   Show my special reading desription.  By default, we set this to
      *   nil to indicate that we should use our default "examine"
      *   description; objects can override this to show a special message
      *   for reading the object as desired.  
      */
     readDesc = nil
 
     /* our reading description when obscured */
     obscuredReadDesc() { gLibMessages.obscuredReadDesc(self); }
 
     /* our reading description in dim light */
     dimReadDesc() { gLibMessages.dimReadDesc(self); }
 
     /* "Read" action */
     dobjFor(Read)
     {
         verify()
         {
             /* give slight preference to an object being held */
             if (!isIn(gActor))
                 logicalRank(80, 'not held');
         }
         action()
         {
             /* 
              *   if we have a special reading description defined, show
              *   it; otherwise, use the same handling as "examine"
              */
             if (propType(&readDesc) != TypeNil)
             {
                 local info;
                 
                 /* 
                  *   Reading requires a transparent sight path and plenty
                  *   of light; in the absence of either of these, we can't
                  *   make out the details. 
                  */
                 info = gActor.bestVisualInfo(self);
                 if (info.trans != transparent)
                     obscuredReadDesc;
                 else if (info.ambient < 3)
                     dimReadDesc;
                 else
                     readDesc;
             }
             else
             {
                 /* 
                  *   we have no special reading description, so use the
                  *   default "examine" handling 
                  */
                 actionDobjExamine();
             }
         }
     }
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   A "consultable" object.  This is an inanimate object that can be
  *   consulted about various topics, almost the way an actor can be asked
  *   about topics.  Examples include individual objects that contain
  *   voluminous information, such as books, phone directories, and maps, as
  *   well as collections of individual information-carrying objects, such
  *   as file cabinets or bookcases.
  *   
  *   A consultable keeps a database of TopicEntry objects; this works in
  *   much the same way as the topic database system that actors use.
  *   Create one or more ConsultTopic objects and place them inside the
  *   Consultable (using the 'location' property, or using the '+' syntax).
  *   When an actor consults the object about a topic, we'll search our
  *   database for a ConsultTopic object that matches the topic and is
  *   currently active, and show the response for the best one we can find.
  *   
  *   From an IF design perspective, consultables have two nice properties.
  *   
  *   First, they hide the boundaries of implementation, by letting the game
  *   *suggest* that there's an untold wealth of information in a particular
  *   book (or whatever) without the need to actually implement all of it.
  *   We only have to show the entries the player specifically asks for, so
  *   the game never has to admit when it's run out of things to show, and
  *   the player can never know for sure that there's not more to find.  Be
  *   careful, though, because this is a double-edge sword, design-wise;
  *   it's easy to abuse this property to hide information gratuitously from
  *   the player.
  *   
  *   Second, consultables help "match impedances" between the narrative
  *   level of detail and the underlying world model.  At the narrative
  *   level, we paint in fairly broad strokes: when we visit a new location,
  *   we describe the *important* features of the setting, not every last
  *   detail.  If the player wants to examine something in closer detail, we
  *   zoom in on that detail, assuming we've implemented it, but it's up to
  *   the player to determine where the attention is focused.  Consultable
  *   objects give us the same capability for books and the like.  With a
  *   consultable, we can describe the way a book looks without immediately
  *   dumping the literal contents of the book onto the screen; but when the
  *   player chooses some aspect of the book to read in detail, we can zoom
  *   in on that page or chapter and show that literal content, if we
  *   choose.
  *   
  *   Also, note that we assume that consultables convey their information
  *   through visual information, such as printed text or a display screen.
  *   Because of this, we by default require that the object be visible to
  *   be consulted.  This might not be appropriate in some cases, such as
  *   Braille books or talking PDA's; to remove the visual condition,
  *   override the pre-condition for the Consult action.  
  */
 class Consultable: Thing, TopicDatabase
     /* 
      *   If they consult us without a topic, just ask for a topic.  Treat
      *   it as logical, but rank it as improbable, in case there's
      *   anything else around that can be consulted without any topic
      *   specified.  
      */
     dobjFor(Consult)
     {
         preCond = [touchObj, objVisible]
         verify() { logicalRank(50, 'need a topic'); }
         action() { askForTopic(ConsultAbout); }
     }
 
     /* consult about a topic */
     dobjFor(ConsultAbout)
     {
         verify() { }
         action()
         {
             /* remember that we're the last object the actor consulted */
             gActor.noteConsultation(self);
 
             /* try handling the topic through our topic database */
             if (!handleTopic(gActor, gTopic, consultConvType, nil))
                 topicNotFound();
         }
     }
 
     /* show the default response for a topic we couldn't find */
     topicNotFound()
     {
         /* 
          *   Report the absence of the topic.  Note that we use an
          *   ordinary, successful report, not a failure report, because
          *   the consultation really did succeed in the sense of the
          *   physical action of consulting: we successfully flipped
          *   through the book, scanned the file cabinet, or whatever.  We
          *   didn't find what we were looking for, but in terms of the
          *   physical action undertaken, we successfully did exactly what
          *   we were asked to do.  
          */
         mainReport(&cannotFindTopicMsg);
     }
 
     /*
      *   Resolve the topic phrase for a CONSULT ABOUT command.  The CONSULT
      *   ABOUT action refers this to the direct object of the action, so
      *   that the direct object can filter the topic match according to
      *   what makes sense for the consultable.
      *   
      *   By default, we resolve the topic phrase a little differently than
      *   we would for conversational commands, such as ASK ABOUT.  By
      *   default, we don't differentiate objects at all based on physical
      *   scope or actor knowledge when deciding on a match for a topic
      *   phrase.  For example, if you create a Consultable representing a
      *   phone book, and the player enters a command like FIND BOB IN PHONE
      *   BOOK, the topic BOB will be found even if the 'bob' object isn't
      *   known to the player character.  The reason for this difference
      *   from ASK ABOUT et al is that consultables are generally the kinds
      *   of objects where, in real life, a person could browse through the
      *   object and come across entries whether or not the person knew
      *   enough to look for them.  For example, you could go through a
      *   phone book and find an entry for "Bob" even if you didn't know
      *   anyone named Bob.
      *   
      *   'lst' is the list of ResolveInfo objects giving the full set of
      *   matches for the vocabulary words; 'np' is the grammar production
      *   object for the topic phrase; and 'resolver' is the TopicResolver
      *   that's resolving the topic phrase.  Note that 'lst' contains
      *   ResolveInfo objects, so to get the game-world object for a given
      *   list entry, use lst[i].obj_.
      *   
      *   We return a ResolvedTopic object that encapsulates the matching
      *   objects.  
      *   
      *   Note that the resolver object can be used to get certain useful
      *   information.  The resolver's getAction() method returns the action
      *   (which you should use instead of gAction, since this routine is
      *   called during the resolution process, not during command
      *   execution); its getTargetActor() method returns the actor
      *   performing the action; and its objInPhysicalScope(obj) method lets
      *   you determine if an object is in physical scope for the actor.  
      */
     resolveConsultTopic(lst, np, resolver)
     {
         /* 
          *   by default, simply return an undifferentiated list with
          *   everything given equal weight, whether known or not, and
          *   whether in scope or not 
          */
         return new ResolvedTopic(lst, [], [], np);
     }
 
     /* 
      *   Our topic entry database for consultatation topics.  This will be
      *   automatically built during initialization from the set of
      *   ConsultTopic objects located within me, so there's usually no
      *   need to initialize this manually.  
      */
     consultTopics = nil
 ;
 
 /*
  *   A consultation topic.  You can place one or more of these inside a
  *   Consultable object (using the 'location' property, or the '+'
  *   notation), to create a database of topics that can be looked up in
  *   the consultable.  
  */
 class ConsultTopic: TopicMatchTopic
     /* include in the consultation list */
     includeInList = [&consultTopics]
 
     /* 
      *   don't set any pronouns for the topic - the consultable itself
      *   should be the pronoun antecedent 
      */
     setTopicPronouns(fromActor, obj) { }
 ;
 
 /* 
  *   A default topic entry for a consultable.  You can include one (or
  *   more) of these in a consultable's database to provide a topic of last
  *   resort that answers to any topics that aren't in the database
  *   themselves.  
  */
 class DefaultConsultTopic: DefaultTopic
     includeInList = [&consultTopics]
     setTopicPronouns(fromActor, obj) { }
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   A common, abstract base class for things that cannot be moved.  You
  *   shouldn't use this class to create game objects directly; you should
  *   always use one of the concrete subclasses, such as Fixture or
  *   Immovable.  This base class doesn't provide the full behavior
  *   necessary to make an object immovable; it's just here as a
  *   programming abstraction for the common elements of all immovable
  *   objects.
  *   
  *   This class has two purposes.  First, it defines some behavior common
  *   to all non-portable objects.  Second, you can test an object to see
  *   if it's based on this class to determine whether it's a portable or
  *   unportable type of Thing.  
  */
 class NonPortable: Thing
     /* 
      *   An immovable objects is not listed in room or container contents
      *   listings.  Since the object is immovable, it's in effect a
      *   permanent feature of its location, so it should be described as
      *   such: either directly as part of its location's description text,
      *   or via its own specialDesc.  
      */
     isListed = nil
     isListedInContents = nil
     isListedInInventory = nil
 
     /* 
      *   By default, if the object's contents would be listed in a direct
      *   examination, then also list them when showing an inventory list,
      *   or describing the enclosing room or an enclosing object.  
      */
     contentsListed = (contentsListedInExamine)
 
     /*
      *   Are my contents within a fixed item that is within the given
      *   location?  Since we're fixed in place, our contents are certainly
      *   within a fixed item, so we merely need to check if we're fixed in
      *   place within the given location.  We are if we're in the given
      *   location or we ourselves are fixed in place in the given location.
      */
     contentsInFixedIn(loc)
     {
         return isDirectlyIn(loc) || isInFixedIn(loc);
     }
 
     /*
      *   Since non-portables aren't carried, their weight and bulk are
      *   largely irrelevant.  Even so, when a non-portable is a component
      *   of another object, or otherwise contained in another object, its
      *   weight and/or bulk can affect the behavior of the parent object.
      *   So, it's simplest to use a default of zero for these so that there
      *   are no surprises about the parent's behavior.  
      */
     weight = 0
     bulk = 0
 
     /*
      *   Non-portable objects can't be held, since they can't be carried.
      *   However, in some cases, it's useful to include non-portable
      *   objects within an actor, such as when creating component parts of
      *   an actor (hands, say).  In these cases, the non-portables aren't
      *   held, but rather are components or similar.  
      */
     isHeldBy(actor) { return nil; }
 
     /*
      *   We're not being held, but if our location is an actor, then we're
      *   as good as held because we're effectively part of the actor.
      */
     meetsObjHeld(actor) { return actor == location; }
 
     /* 
      *   showing an immovable to someone simply requires that it be in
      *   sight: we're not holding it up to show it, we're simply pointing
      *   it out 
      */
     dobjFor(ShowTo) { preCond = [objVisible] }
 
     /*
      *   Thing decreases the likelihood that we want to examine an object
      *   when the object isn't being held.  That's fine for portable
      *   objects, but nonportables can never be held, so we don't want that
      *   decrease in logicalness. 
      */
     dobjFor(Examine)
     {
         /* override Thing's likelihood downgrade for un-held items */
         verify() { }
     }
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   A "fixture," which is something that's obviously a part of the room.
  *   These objects cannot be removed from their containers.  This class is
  *   meant for permanent features of rooms that obviously cannot be moved
  *   to a new container, such as walls, floors, doors, built-in bookcases,
  *   light switches, buildings, and the like.
  *   
  *   The important feature of a Fixture is that it's *obvious* that it's
  *   part of its container, so it should be safe to assume that a character
  *   normally wouldn't even try to take it or move it.  For objects that
  *   might appear portable but turn out to be immovable, other classes are
  *   more appropriate: use Heavy for objects that are immovable simply
  *   because they're very heavy, for example, or Immovable for objects that
  *   are immovable for some non-obvious reason.  
  */
 class Fixture: NonPortable
     /* 
      *   Hide fixtures from "all" for certain commands.  Fixtures are
      *   obviously part of the location, so a reaonable person wouldn't
      *   even consider trying to do things like take them or move them.  
      */
     hideFromAll(action)
     {
         return (action.ofKind(TakeAction)
                 || action.ofKind(DropAction)
                 || action.ofKind(PutInAction)
                 || action.ofKind(PutOnAction));
     }
 
     /* don't hide from defaults, though */
     hideFromDefault(action) { return nil; }
 
     /* a fixed item can't be moved by an actor action */
     verifyMoveTo(newLoc)
     {
         /* it's never possible to do this */
         illogical(cannotMoveMsg);
     }
 
     /* 
      *   a fixed item can't be taken - this would be caught by
      *   verifyMoveTo anyway, but provide a more explicit message when a
      *   fixed item is explicitly taken 
      */
     dobjFor(Take) { verify() { illogical(cannotTakeMsg); }}
     dobjFor(TakeFrom) { verify() { illogical(cannotTakeMsg); }}
 
     /* fixed objects can't be put anywhere */
     dobjFor(PutIn) { verify() { illogical(cannotPutMsg); }}
     dobjFor(PutOn) { verify() { illogical(cannotPutMsg); }}
     dobjFor(PutUnder) { verify() { illogical(cannotPutMsg); }}
     dobjFor(PutBehind) { verify() { illogical(cannotPutMsg); }}
 
     /* fixed objects can't be pushed, pulled, or moved */
     dobjFor(Push) { verify() { illogical(cannotMoveMsg); }}
     dobjFor(Pull) { verify() { illogical(cannotMoveMsg); }}
     dobjFor(Move) { verify() { illogical(cannotMoveMsg); }}
     dobjFor(MoveWith) { verify() { illogical(cannotMoveMsg); }}
     dobjFor(MoveTo) { verify() { illogical(cannotMoveMsg); }}
     dobjFor(PushTravel) { verify() { illogical(cannotMoveMsg); }}
     dobjFor(ThrowAt) { verify() { illogical(cannotMoveMsg); }}
     dobjFor(ThrowDir) { verify() { illogical(cannotMoveMsg); }}
 
     /*
      *   The messages to use for illogical messages.  These can be
      *   overridden with new properties (of playerActionMessages and the
      *   like), or simply with single-quoted strings to display.  
      */
     cannotTakeMsg = &cannotTakeFixtureMsg
     cannotMoveMsg = &cannotMoveFixtureMsg
     cannotPutMsg = &cannotPutFixtureMsg
 
     /*
      *   A component can be said to be owned by its location's owner or by
      *   its location. 
      */
     isOwnedBy(obj)
     {
         /* 
          *   if I'm owned by the object under the normal rules, then we
          *   won't say otherwise 
          */
         if (inherited(obj))
             return true;
 
         /* 
          *   we can be said to be owned by our location, since we're a
          *   direct and permanent part of the location 
          */
         if (obj == location)
             return true;
 
         /* 
          *   if my location is owned by the given object, consider
          *   ourselves owned by it as well, as we're an extension of our
          *   location 
          */
         if (location != nil && location.isOwnedBy(obj))
             return true;
 
         /* we didn't find anything that establishes ownership */
         return nil;
     }
 ;
 
 /*
  *   A component object.  These objects cannot be removed from their
  *   containers because they are permanent features of other objects, which
  *   may themselves be portable: the hands of a watch, a tuning dial on a
  *   radio.  This class behaves essentially the same way as Fixture, but
  *   its messages are more suitable for objects that are component parts of
  *   other objects rather than fixed features of rooms.  
  */
 class Component: Fixture
     /* a component cannot be removed from its container by an actor action */
     verifyMoveTo(newLoc)
     {
         /* it's never possible to do this */
         illogical(&cannotMoveComponentMsg, location);
     }
 
     /*
      *   Hide components from EXAMINE ALL, as well as any commands hidden
      *   from ALL for ordinary fixtures.  Components are small parts of
      *   larger objects, so when we EXAMINE ALL, it's enough to examine the
      *   larger objects of which we're a part; we don't want components to
      *   show up separately in these cases.  
      */
     hideFromAll(action)
     {
         /* hide from EXAMINE ALL, plus anything the base class hides */
         return (action.ofKind(ExamineAction)
                 || inherited(action));
     }
 
     /* 
      *   We are a component of our direct cotnainer, and we're indirectly a
      *   component of anything that it's a component of.  
      */
     isComponentOf(obj)
     {
         return (obj == location
                 || (location != nil && location.isComponentOf(obj)));
     }
 
     /*
      *   Consider ourself to be held by the given actor if we're a
      *   component of the actor.  
      */
     meetsObjHeld(actor) { return isComponentOf(actor); }
 
     /* a component cannot be taken separately */
     dobjFor(Take)
         { verify() { illogical(&cannotTakeComponentMsg, location); }}
     dobjFor(TakeFrom)
         { verify() { illogical(&cannotTakeComponentMsg, location); }}
 
     /* a component cannot be separately put somewhere */
     dobjFor(PutIn)
         { verify() { illogical(&cannotPutComponentMsg, location); }}
     dobjFor(PutOn)
         { verify() { illogical(&cannotPutComponentMsg, location); }}
     dobjFor(PutUnder)
         { verify() { illogical(&cannotPutComponentMsg, location); }}
     dobjFor(PutBehind)
         { verify() { illogical(&cannotPutComponentMsg, location); }}
 ;
 
 /*
  *   A "secret fixture" is a kind of fixture that we use for internal
  *   implementation purposes, and which we don't intend to be visible to
  *   the player.  Objects of this type usually have no vocabulary, since we
  *   don't want the player to be able to refer to them.  
  */
 class SecretFixture: Fixture
     /* 
      *   this kind of object is internal to the game's implementation, so
      *   we don't want it to show up in "all" lists 
      */
     hideFromAll(action) { return true; }
 ;
 
 /*
  *   A fixture that uses the same custom message for taking, moving, and
  *   putting.  In many cases, it's useful to customize the message for a
  *   fixture, using the same custom message for all sorts of moving.  Just
  *   override cannotTakeMsg, and the other messages will copy it.  
  */
 class CustomFixture: Fixture
     cannotMoveMsg = (cannotTakeMsg)
     cannotPutMsg = (cannotTakeMsg)
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   An Immovable is an object that can't be moved, but not because it's
  *   obviously a fixture or component of another object.  This class is
  *   suitable for things like furniture, which are in principle portable
  *   but which actors aren't actually allowed to pick up or move around.
  *   
  *   Note that Immovable is a lot like Fixture.  The difference is that
  *   Fixture is for objects that are *obviously* fixed in place by their
  *   very nature, whereas Immovable is for objects that common sense would
  *   tell us are portable, but which the game doesn't in fact allow the
  *   player to move.
  *   
  *   The practical difference between Immovable and Fixture is that Fixture
  *   considers taking or moving to be illogical actions, whereas Immovable
  *   considers these actions logical but simply doesn't allow them.  To be
  *   more specific, Fixture disallows taking and moving in the verify()
  *   methods for those actions, while Immovable disallows the actions in
  *   the action() methods.  This means, for example, that Fixture objects
  *   will be removed from consideration during the noun resolution phase
  *   when there are more logical choices.
  */
 class Immovable: NonPortable
     /* an Immovable can't be taken */
     dobjFor(Take) { action() { reportFailure(cannotTakeMsg); }}
     dobjFor(TakeFrom) { check() { failCheck(cannotTakeMsg); }}
 
     /* Immovables can't be put anywhere */
     dobjFor(PutIn) { check() { failCheck(cannotPutMsg); }}
     dobjFor(PutOn) { check() { failCheck(cannotPutMsg); }}
     dobjFor(PutUnder) { check() { failCheck(cannotPutMsg); }}
     dobjFor(PutBehind) { check() { failCheck(cannotPutMsg); }}
 
     /* Immovables can't be pushed, pulled, or otherwise moved */
     dobjFor(Drop) { action() { reportFailure(cannotMoveMsg); }}
     dobjFor(Push) { action() { reportFailure(cannotMoveMsg); }}
     dobjFor(Pull) { action() { reportFailure(cannotMoveMsg); }}
     dobjFor(Move) { action() { reportFailure(cannotMoveMsg); }}
     dobjFor(MoveWith) { check() { failCheck(cannotMoveMsg); }}
     dobjFor(MoveTo) { check() { failCheck(cannotMoveMsg); }}
     dobjFor(PushTravel) { action() { reportFailure(cannotMoveMsg); }}
     dobjFor(ThrowAt) { verify() { illogical(cannotMoveMsg); }}
     dobjFor(ThrowDir) { verify() { illogical(cannotMoveMsg); }}
     dobjFor(Turn)
     {
         verify() { logicalRank(50, 'turn heavy'); }
         action() { reportFailure(cannotMoveMsg); }
     }
 
     /*
      *   The messages to use for the failure messages.  These can be
      *   overridden with new properties (of playerActionMessages and the
      *   like), or simply with single-quoted strings to display.  
      */
     cannotTakeMsg = &cannotTakeImmovableMsg
     cannotMoveMsg = &cannotMoveImmovableMsg
     cannotPutMsg = &cannotPutImmovableMsg
 ;
 
 /*
  *   An immovable that uses the same custom message for taking, moving, and
  *   putting.  In many cases, it's useful to customize the message for an
  *   immovable, using the same custom message for all sorts of moving.
  *   Just override cannotTakeMsg, and the other messages will copy it. 
  */
 class CustomImmovable: Immovable
     cannotMoveMsg = (cannotTakeMsg)
     cannotPutMsg = (cannotTakeMsg)
 ;
 
 /*
  *   Heavy: an object that's immovable because it's very heavy.  This is
  *   suitable for things like large boulders, heavy furniture, or the like:
  *   things that aren't nailed down, but nonetheless are too heavy to be
  *   carried or otherwise move.
  *   
  *   This is a simple specialization of Immovable; the only thing we change
  *   is the messages we use to describe why the object can't be moved.  
  */
 class Heavy: Immovable
     cannotTakeMsg = &cannotTakeHeavyMsg
     cannotMoveMsg = &cannotMoveHeavyMsg
     cannotPutMsg = &cannotPutHeavyMsg
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Decoration.  This is an object that is included for scenery value but
  *   which has no other purpose, and which the author wants to make clear
  *   is not important.  We use the catch-all action routine to respond to
  *   any command on this object with a flat "that's not important"
  *   message, so that the player can plainly see that there's no point
  *   wasting any time trying to manipulate this object.
  *   
  *   We use the "default" catch-all verb verify handling to report our
  *   "that's not important" message, so a decoration can be made
  *   responsive to specific verbs simply by defining an action handler for
  *   those verbs.  
  */
 class Decoration: Fixture
     /* don't include decorations in 'all' */
     hideFromAll(action) { return true; }
 
     /* don't hide from defaults */
     hideFromDefault(action) { return nil; }
 
     /* 
      *   use the default response "this object isn't important" when we're
      *   used as either a direct or indirect object 
      */
     dobjFor(Default)
     {
         verify() { illogical(notImportantMsg, self); }
     }
     iobjFor(Default)
     {
         verify() { illogical(notImportantMsg, self); }
     }
 
     /* use the standard not-important message for decorations */
     notImportantMsg = &decorationNotImportantMsg
 
     /*
      *   The catch-all Default verifier makes all actions illogical, but we
      *   can override this to allow specific actions by explicitly defining
      *   them here so that they hide the Default verify handlers.  In
      *   addition, give decorations a reduced logical rank, so that any
      *   in-scope non-decoration object with similar vocabulary will be
      *   matched for an Examine command ahead of a decoration.  
      */
     dobjFor(Examine)
         { verify() { inherited(); logicalRank(70, 'decoration'); } }
 
     /* 
      *   likewise for LISTEN TO and SMELL, which are the auditory and
      *   olfactory equivalents of EXAMINE 
      */
     dobjFor(ListenTo)
         { verify() { inherited(); logicalRank(70, 'decoration'); } }
     dobjFor(Smell)
         { verify() { inherited(); logicalRank(70, 'decoration'); } }
 
     /* likewise for READ */
     dobjFor(Read)
         { verify() { inherited(); logicalRank(70, 'decoration'); } }
 
     /* likewise for LOOK IN */
     dobjFor(LookIn)
         { verify() { inherited(); logicalRank(70, 'decoration'); } }
 
     /* the default LOOK IN response is our standard "that's not important" */
     lookInDesc { mainReport(&notImportantMsg, self); }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   An "unthing" is an object that represents the *absence* of an object.
  *   It's occasionally useful to respond specially when the player mentions
  *   an object that isn't present, especially when the player is likely to
  *   assume that something is present.
  *   
  *   An unthing is essentially a decoration, but we use a customized
  *   message that says "that isn't here" rather than "that isn't
  *   important".  
  */
 class Unthing: Decoration
     /* 
      *   The message to display when the player refers to this object.
      *   This can be a library message property, or a single-quoted string.
      *   This message will probably always be overridden in practice, since
      *   the point of this class is to provide a more specific explanation
      *   of why the object isn't here.  
      */
     notHereMsg = &unthingNotHereMsg
 
     /* an Unthing shouldn't be picked as a default */
     hideFromDefault(action) { return true; }
 
     /* 
      *   by default, use our 'not here' message for our descriptions (in
      *   all of the standard senses) 
      */
     basicExamine() { mainReport(notHereMsg, self); }
     basicExamineListen(explicit)
     {
         if (explicit)
             mainReport(notHereMsg, self);
     }
     basicExamineSmell(explicit)
     {
         if (explicit)
             mainReport(notHereMsg, self);
     }
 
     /* use our custom message for the inherited Decoration responses */
     notImportantMsg = (notHereMsg)
 
     /*
      *   Because we're not actually here, use custom error messages when
      *   we're used as a possessive or locational qualifier.  The standard
      *   messages say things like "Bob doesn't appear to have that" or "You
      *   don't see that in the box," but these don't make sense for an
      *   Unthing - we're not actually here, so we can't "appear" or "seem"
      *   to own or contain anything.  Instead, we need to indicate that the
      *   qualifying object itself (i.e., 'self') isn't here at all.  
      */
     throwNoMatchForPossessive(txt) { throwUnthingAsQualifier(); }
     throwNoMatchForLocation(txt) { throwUnthingAsQualifier(); }
     throwNothingInLocation() { throwUnthingAsQualifier(); }
 
     /* 
      *   throw a generic message when we're used as a qualifier - we'll
      *   simply get our "not here" message and display that 
      */
     throwUnthingAsQualifier()
     {
         local msg;
         
         /* 
          *   resolve our "not here" message to a string - we need to do
          *   this here, since we're too early in the parsing sequence for
          *   the normal "mainResult" type of processing 
          */
         msg = MessageResult.resolveMessageText([self], &notHereMsg, [self]);
 
         /* throw a parser exception that will display this literal text */
         throw new ParseFailureException(&parserErrorString, msg);
     }
 
     /* 
      *   if there's anything at all in a resolve list other than me, always
      *   remove me 
      */
     filterResolveList(lst, action, whichObj, np, requiredNum)
     {
         /* if the list has anything else in it, remove myself */
         if (lst.length() != 1)
             lst = lst.removeElementAt(lst.indexWhich({x: x.obj_ == self}));
 
         /* return the list */
         return lst;
     }
 
     /* 
      *   trying to given an order to an Unthing acts the same way as any
      *   other kind of interaction 
      */
     acceptCommand(issuingActor) { mainReport(notHereMsg, self); }
 ;
     
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Distant item.  This is an object that's too far away to manipulate,
  *   but can be seen.  This is useful for scenery objects that are at a
  *   great distance within a large location.
  *   
  *   A Distant item is essentially just like a decoration, but the default
  *   message is different.  Note that this class is based on Fixture, which
  *   means that it should be *obvious* that the object is too far away to
  *   take or move.  
  */
 class Distant: Fixture
     /* don't include in 'all' */
     hideFromAll(action) { return true; }
 
     dobjFor(Default)
     {
         verify() { illogical(&tooDistantMsg, self); }
     }
     iobjFor(Default)
     {
         verify() { illogical(&tooDistantMsg, self); }
     }
 
     /* 
      *   Explicitly allow examining and listening to a Distant item.  To
      *   do this, override the 'verify' methods explicitly; we only need
      *   to inherit the base class handling, but we need to explicitly do
      *   so to 'override' the catch-all default handlers. 
      */
     dobjFor(Examine) { verify { inherited() ; } }
     dobjFor(ListenTo) { verify() { inherited(); } }
 
     /* similarly, allow showing a distant item */
     dobjFor(ShowTo) { verify() { inherited(); } }
 ;
 
 /*
  *   Out Of Reach - this is a special mix-in that can be used to create an
  *   object that places its *contents* out of reach under customizable
  *   conditions, and can optionally place itself out of reach as well.
  */
 class OutOfReach: object
     checkTouchViaPath(obj, dest, op)
     {
         /* check how we're traversing the object */
         if (op == PathTo)
         {
             /* 
              *   we're reaching from outside for this object itself -
              *   check to see if the source can reach me
              */
             if (!canObjReachSelf(obj))
                 return new CheckStatusFailure(
                     cannotReachFromOutsideMsg(dest), dest);
         }
         else if (op == PathIn)
         {
             /* 
              *   we're reaching in to touch one of my contents - check to
              *   see if the source object is within reach of my contents 
              */
             if (!canObjReachContents(obj))
                 return new CheckStatusFailure(
                     cannotReachFromOutsideMsg(dest), dest);
         }
         else if (op == PathOut)
         {
             local ok;
             
             /*
              *   We're reaching out.  If we're reaching for the object
              *   itself, check to see if we're reachable from within;
              *   otherwise, check to see if we can reach objects outside
              *   us from within.  
              */
             if (dest == self)
                 ok = canReachSelfFromInside(obj);
             else
                 ok = canReachFromInside(obj, dest);
 
             /* if we can't reach the object, say so */
             if (!ok)
                 return new CheckStatusFailure(
                     cannotReachFromInsideMsg(dest), dest);
         }
         
         /* if we didn't find a problem, allow the operation */
         return checkStatusSuccess;
     }
 
     /* 
      *   The message to use to indicate that we can't reach an object,
      *   because the actor is outside me and the target is inside, or vice
      *   versa.  Each of these can return a property ID giving an actor
      *   action message property, or can simply return a string with the
      *   message text.  
      */
     cannotReachFromOutsideMsg(dest) { return &tooDistantMsg; }
     cannotReachFromInsideMsg(dest) { return  &tooDistantMsg; }
 
     /*
      *   Determine if the given object can reach my contents.  'obj' is
      *   the object (usually an actor) attempting to reach my contents
      *   from outside of me.
      *   
      *   By default, we'll return nil, so that nothing within me can be
      *   reached from anyone outside.  This can be overridden to allow my
      *   contents to become reachable from some external locations but not
      *   others; for example, a high shelf could allow an actor standing
      *   on a chair to reach my contents. 
      */
     canObjReachContents(obj) { return nil; }
 
     /*
      *   Determine if the given object can reach me.  'obj' is the object
      *   (usually an actor) attempting to reach this object.
      *   
      *   By default, make this object subject to the same rules as its
      *   contents.  
      */
     canObjReachSelf(obj) { return canObjReachContents(obj); }
 
     /*
      *   Determine if the given object outside of me is reachable from
      *   within me.  'obj' (usually an actor) is attempting to reach
      *   'dest'.
      *   
      *   By default, we return nil, so nothing outside of me is reachable
      *   from within me.  This can be overridden as needed.  This should
      *   usually behave symmetrically with canObjReachContents().  
      */
     canReachFromInside(obj, dest) { return nil; }
 
     /* 
      *   Determine if we can reach this object itself from within.  This
      *   is used when 'obj' tries to touch this object when 'obj' is
      *   located within this object.
      *   
      *   By default, we we use the same rules as we use to reach an
      *   external object from within.  
      */
     canReachSelfFromInside(obj) { return canReachFromInside(obj, self); }
 
     /*
      *   We cannot implicitly remove this obstruction, so simply return
      *   nil when asked.  
      */
     tryImplicitRemoveObstructor(sense, obj) { return nil; }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   A Fill Medium - this is the class of object returned from
  *   Thing.fillMedium().  
  */
 class FillMedium: Thing
     /*
      *   Get the transparency sensing through this medium. 
      */
     senseThru(sense)
     {
         /* 
          *   if I have a meterial, use its transparency; otherwise, we're
          *   transparent 
          */
         return (material != nil ? material.senseThru(sense) : transparent);
     }
 
     /* my material */
     material = nil
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Base multi-location item with automatic initialization.  This is the
  *   base class for various multi-located object classes.
  *   
  *   We provide four ways of initializing a multi-located object's set of
  *   locations.
  *   
  *   First, the object can simply enumerate the locations explicitly, by
  *   setting the 'locationList' property to the list of locations.
  *   
  *   Second, the object can indicate that it's located in every object of a
  *   given class, by setting the 'initialLocationClass' property to the
  *   desired class.
  *   
  *   Third, the object can define a rule that specifies which objects are
  *   its initial locations, by defining the 'isInitiallyIn(obj)' method to
  *   return true if 'obj' is an initial location, nil if not.  This can be
  *   combined with the 'initialLocationClass' mechanism: if
  *   'initialLocationClass' is non-nil, then only objects of the given
  *   class will be tested with 'isInitiallyIn()'; if 'initialLocationClass'
  *   is nil, then every object in the entire game will be tested.
  *   
  *   Fourth, you can override the method buildLocationList() to build an
  *   return the initial list of locations.  You can use this approach if
  *   you have a complex set of rules for determining the initial location
  *   list, and none of the above approaches are flexible enough to
  *   implement it.  If you override buildLocationList(), simply compute and
  *   return the list of initial locations; the library will automatically
  *   call the method during pre-initialization.
  *   
  *   If you don't define any of these, then the object simply has no
  *   initial locations by default.  
  */
 class BaseMultiLoc: object
     /* 
      *   The location list.  Instances can override this to manually
      *   enumerate our initial locations.  By default, we'll call
      *   buildLocationList() the first time this is invoked, and store the
      *   result. 
      */
     locationList = perInstance(buildLocationList())
 
     /*
      *   The class of our initial locations.  If this is nil, then our
      *   default buildLocationList() method will test every object in the
      *   entire game with our isInitiallyIn() method; otherwise, we'll test
      *   only objects of the given class.  
      */
     initialLocationClass = nil
 
     /*
      *   Test an object for inclusion in our initial location list.  By
      *   default, we'll simply return true to include every object.  We
      *   return true by default so that an instance can merely specify a
      *   value for initialLocationClass in order to place this object in
      *   every instance of the given class.
      */
     isInitiallyIn(obj) { return true; }
 
     /*
      *   Build my list of locations, and return the list.  This default
      *   implementation looks for an 'initialLocationClass' property value,
      *   and if one is found, looks at every object of that class;
      *   otherwise, it looks at every object in the entire game.  In either
      *   case, each object is then passed to our isInitiallyIn() method,
      *   and is included in our result list if isInitiallyIn() returns
      *   true.  
      */
     buildLocationList()
     {
         local lst;
 
         /*
          *   If the object doesn't define any of the standard rules, which
          *   it would do by overriding initialLocationClass and/or
          *   isInitiallyIn(), then simply return an empty list.  We take
          *   the absence of overrides for any of the rules to mean that the
          *   object simply has no initial locations.  
          */
         if (initialLocationClass == nil
             && !overrides(self, BaseMultiLoc, &isInitiallyIn))
             return [];
 
         /* we have nothing in our list yet */
         lst = new Vector(16);
 
         /*
          *   if initialLocationClass is defined, loop over all objects of
          *   that class; otherwise, loop over all objects
          */
         if (initialLocationClass != nil)
         {
             /* loop over all instances of the given class */
             for (local obj = firstObj(initialLocationClass) ; obj != nil ;
                  obj = nextObj(obj, initialLocationClass))
             {
                 /* if the object passes the test, include it */
                 if (isInitiallyIn(obj))
                     lst.append(obj);
             }
         }
         else
         {
             /* loop over all objects */
             for (local obj = firstObj() ; obj != nil ; obj = nextObj(obj))
             {
                 /* if the object passes the test, include it */
                 if (isInitiallyIn(obj))
                     lst.append(obj);
             }
         }
 
         /* return the list of locations */
         return lst.toList();
     }
 
     /* determine if I'm in a given object, directly or indirectly */
     isIn(obj)
     {
         /* first, check to see if I'm directly in the given object */
         if (isDirectlyIn(obj))
             return true;
 
         /*
          *   Look at each object in my location list.  For each location
          *   object, if the location is within the object, I'm within the
          *   object.
          */
         return locationList.indexWhich({loc: loc.isIn(obj)}) != nil;
     }
 
     /* determine if I'm directly in the given object */
     isDirectlyIn(obj)
     {
         /*
          *   we're directly in the given object only if the object is in
          *   my list of immediate locations
          */
         return (locationList.indexOf(obj) != nil);
     }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   MultiLoc: this class can be multiply inherited by any object that
  *   must exist in more than one place at a time.  To use this class, put
  *   it BEFORE Thing (or any subclass of Thing) in the object's superclass
  *   list, to ensure that we override the default containment
  *   implementation for the object.
  *   
  *   Note that a MultiLoc object appears *in its entirety* in each of its
  *   locations.  This means that MultiLoc is most suitable for a couple of
  *   specific situations:
  *   
  *   - several locations overlap slightly so that they include a common
  *   object: a large statue at the center of a public square, for example;
  *   
  *   - an object forms a sense connection among its location: a window;
  *   
  *   - a distant object that is seen in its entirety from several
  *   locations: the moon, say, or a mountain range.
  *   
  *   Note that MultiLoc is NOT suitable for cases where an object spans
  *   several locations but isn't contained entirely in any one of them:
  *   it's not good for something like a rope or a river, for example.
  *   MultiLoc also isn't good for cases where you simply want to avoid
  *   creating a bunch of repeated decorations in different locations.
  *   MultiLoc isn't good for these cases because a MultiLoc is treated as
  *   though it exists ENTIRELY and SIMULTANEOUSLY in all of its locations,
  *   which means that all of its sense information and internal state is
  *   shared among all of its locations.
  *   
  *   MultiInstance is better than MultiLoc for cases where you want to
  *   share a decoration object across several locations.  MultiInstance is
  *   better because it creates individual copies of the object in the
  *   different locations, so each copy has its own separate sense
  *   information and its own separate identity.
  *   
  *   MultiFaceted is better for objects that span several locations, such
  *   as a river or a long rope.  Like MultiInstance, MultiFaceted creates
  *   a separate copy in each location; in addition, MultiFaceted relates
  *   the copies together as "facets" of the same object, so that the
  *   parser knows they're all actually parts of one larger object.  
  */
 class MultiLoc: BaseMultiLoc
     /*
      *   Initialize my location's contents list - add myself to my
      *   container during initialization
      */
     initializeLocation()
     {
         /* add myself to each of my container's contents lists */
         locationList.forEach({loc: loc.addToContents(self)});
     }
 
     /*
      *   Re-initialize the location list.  This calls buildLocationList()
      *   to re-evaluate the location rules, then updates the locationList
      *   to match the new results.  We'll remove the MultiLoc from any old
      *   locations that are no longer part of the location list, and we'll
      *   add it to any new locations that weren't previously in the
      *   location list.  You can call this at any time to update the
      *   MutliLoc's presence to reflect applying our location rules to the
      *   current game state.
      *   
      *   Note that this doesn't trigger any moveInto notifications.  This
      *   routine is a re-initialization rather than an in-game action, so
      *   it's not meant to behave as though an actor in the game were
      *   walking around moving the MultiLoc around; thus no notifications
      *   are sent.  Note also that we attempt to minimize our work by
      *   computing the "delta" from the old state - hence we only move the
      *   MultiLoc into containers it wasn't in previously, and we only
      *   remove it from existing containers that it's no longer in.  
      */
     reInitializeLocation()
     {
         local newList;
 
         /* build the new location list */
         newList = buildLocationList();
 
         /*
          *   Update any containers that are not in the intersection of the
          *   two lists.  Note that we don't simply move ourselves out of
          *   the old list and into the new list, because the two lists
          *   could have common members; to avoid unnecessary work that
          *   might result from removing ourselves from a container and
          *   then adding ourselves right back in to the same container, we
          *   only notify containers when we're actually moving out or
          *   moving in. 
          */
 
         /* 
          *   For each item in the old list, if it's not in the new list,
          *   notify the old container that we're being removed.
          */
         foreach (local loc in locationList)
         {
             /* if it's not in the new list, remove me from the container */
             if (newList.indexOf(loc) == nil)
                 loc.removeFromContents(self);
         }
 
         /* 
          *   for each item in the new list, if we weren't already in this
          *   location, add ourselves to the location 
          */
         foreach (local loc in newList)
         {
             /* if it's not in the old list, add me to the new container */
             if (!isDirectlyIn(loc) == nil)
                 loc.addToContents(self);
         }
         
         /* make the new location list current */
         locationList = newList;
     }
 
     /*
      *   Note that we don't need to override any of the contents
      *   management methods, since we provide special handling for our
      *   location relationships, not for our contents relationships.
      */
 
     /* save my location for later restoration */
     saveLocation()
     {
         /* return my list of locations */
         return locationList;
     }
 
     /* restore a previously saved location */
     restoreLocation(oldLoc)
     {
         /* remove myself from each current location not in the saved list */
         foreach (local cur in locationList)
         {
             /* 
              *   if this present location isn't in the saved list, remove
              *   myself from the location 
              */
             if (oldLoc.indexOf(cur) == nil)
                 cur.removeFromContents(self);
         }
 
         /* add myself to each saved location not in the current list */
         foreach (local cur in oldLoc)
         {
             /* if I'm not already in this location, add me to it */
             if (locationList.indexOf(cur) == nil)
                 cur.addToContents(self);
         }
 
         /* set my own list to the original list */
         locationList = oldLoc;
     }
 
     /*
      *   Basic routine to move this object into a given single container.
      *   Removes the object from all of its other containers.  Performs no
      *   notifications.  
      */
     baseMoveInto(newContainer)
     {
         /* remove myself from all of my current contents */
         locationList.forEach({loc: loc.removeFromContents(self)});
 
         /* set my location list to include only the new location */
         if (newContainer != nil)
         {
             /* set my new location */
             locationList = [newContainer];
 
             /* add myself to my new container's contents */
             newContainer.addToContents(self);
         }
         else
         {
             /* we have no new locations */
             locationList = [];
         }
     }
 
     /*
      *   Add this object to a new location - base version that performs no
      *   notifications.  
      */
     baseMoveIntoAdd(newContainer)
     {
         /* add the new container to my list of locations */
         locationList += newContainer;
 
         /* add myself to my new container's contents */
         newContainer.addToContents(self);
     }
 
     /*
      *   Add this object to a new location.
      */
     moveIntoAdd(newContainer)
     {
         /* notify my new container that I'm about to be added */
         if (newContainer != nil)
             newContainer.sendNotifyInsert(self, newContainer, &notifyInsert);
 
         /* perform base move-into-add operation */
         baseMoveIntoAdd(newContainer);
         
         /* note that I've been moved */
         moved = true;
     }
 
     /*
      *   Base routine to move myself out of a given container.  Performs
      *   no notifications. 
      */
     baseMoveOutOf(cont)
     {
         /* remove myself from this container's contents list */
         cont.removeFromContents(self);
 
         /* remove this container from my location list */
         locationList -= cont;
     }
 
     /*
      *   Remove myself from a given container, leaving myself in any other
      *   containers.
      */
     moveOutOf(cont)
     {
         /* if I'm not actually directly in this container, do nothing */
         if (!isDirectlyIn(cont))
             return;
 
         /* 
          *   notify this container (and only this container) that we're
          *   being removed from it 
          */
         cont.sendNotifyRemove(obj, nil, &notifyRemove);
 
         /* perform base operation */
         baseMoveOutOf(cont);
 
         /* note that I've been moved */
         moved = true;
     }
 
     /*
      *   Call a function on each container.  We'll invoke the function as
      *   follows for each container 'cont':
      *   
      *   (func)(cont, args...)  
      */
     forEachContainer(func, [args])
     {
         /* call the function for each location in our list */
         foreach(local cur in locationList)
             (func)(cur, args...);
     }
 
     /* 
      *   Call a function on each connected container.  By default, we
      *   don't connect our containers for sense purposes, so we do nothing
      *   here. 
      */
     forEachConnectedContainer(func, ...) { }
 
     /* 
      *   get a list of my connected containers; by default, we don't
      *   connect our containers, so this is an empty list 
      */
     getConnectedContainers = []
 
     /* 
      *   Clone this object's contents for inclusion in a MultiInstance's
      *   contents tree.  A MultiLoc is capable of being in multiple places
      *   at once, so we can just use our original contents tree as is.
      */
     cloneMultiInstanceContents(loc) { }
 
     /*
      *   Create a clone of this object for inclusion in a MultiInstance's
      *   contents tree.  We don't actually need to make a copy of the
      *   object, because a MultiLoc can be in several locations
      *   simultaneously; all we need to do is add ourselves to the new
      *   location. 
      */
     cloneForMultiInstanceContents(loc)
     {
         /* add myself into the new container */
         baseMoveIntoAdd(loc);
     }
 
     /*
      *   Add the direct containment connections for this item to a lookup
      *   table. 
      *   
      *   A MultiLoc does not, by default, connect its multiple locations
      *   together.  This means that if we're traversing in from a point of
      *   view outside the MultiLoc object, we don't add any of our other
      *   containers to the connection table.  However, the MultiLoc
      *   itself, and its contents, *can* see out to all of its locations;
      *   so if we're traversing from a point of view inside self, we will
      *   add all of our containers to the connection list. 
      */
     addDirectConnections(tab)
     {
         /* add myself */
         tab[self] = true;
 
         /* add my contents */
         foreach (local cur in contents)
         {
             if (tab[cur] == nil)
                 cur.addDirectConnections(tab);
         }
         
         /* 
          *   If we're traversing from the outside in, don't connect any of
          *   our other containers.  However, if we're traversing from a
          *   point of view inside us, we do get to see out to all of our
          *   containers.
          */
         if (senseTmp.pointOfView.isIn(self))
         {
             /* add my locations */
             foreach (local cur in locationList)
             {
                 if (tab[cur] == nil)
                     cur.addDirectConnections(tab);
             }
         }
     }
 
     /*
      *   Transmit ambient energy to my location or locations.  Note that
      *   even though we don't by default shine light from one of our
      *   containers to another, we still shine light from within me to
      *   each of our containers.  
      */
     shineOnLoc(sense, ambient, fill)
     {
         /* shine on each of my containers and their immediate children */
         foreach (local cur in locationList)
             cur.shineFromWithin(self, sense, ambient, fill);
     }
 
 
     /*
      *   Build a sense path to my location or locations.  Note that even
      *   though we don't by default connect our different containers
      *   together, we still build a sense path from within to outside,
      *   because we can see from within out to all of our containers.  
      */
     sensePathToLoc(sense, trans, obs, fill)
     {
         /* build a path to each of my containers and their children */
         foreach (local cur in locationList)
             cur.sensePathFromWithin(self, sense, trans, obs, fill);
     }
 
 
     /*
      *   Get the drop destination.  The default implementation in Thing
      *   won't work for us, because it delegates to its location to find
      *   the drop destination; we can't do that because we could have
      *   several locations.  To figure out which of our multiple locations
      *   to delegate to, we'll look for 'self' in the supplied sense path;
      *   if we can find it, and the previous path element is a container or
      *   peer of ours, then we'll delegate to that container, because it's
      *   the "side" we approached from.  If there's no path, or if we're
      *   not preceded in the path by a container of ours, we'll arbitrarily
      *   delegate to our first container.
      *   
      *   Note that when we don't have a path, or there's no container of
      *   ours preceding us in the path, the object being dropped must be
      *   starting inside us.  It would be highly unusual for this to happen
      *   with a multi-location object, because MutliLoc isn't designed for
      *   use as a "nested room" or the like.  However, it's not an
      *   impossible situation; if the game does want to create such a
      *   scenario, then the game simply needs to override this routine so
      *   that it does whatever makes sense in the game scenario.  There's
      *   no general way to handle such situations, but it should be
      *   possible to determine the correct handling for specific scenarios.
      */
     getDropDestination(obj, path)
     {
         local idx;
 
         /* 
          *   if there's no path, get the ordinary "touch" path from the
          *   current actor to us, since this is how the actor would reach
          *   out and touch this object 
          */
         if (path == nil)
             path = gActor.getTouchPathTo(self);
         
         /* 
          *   if there's a path, check to see if we're in it; if so, and
          *   we're not the first element, and the preceding element is a
          *   container or peer of ours, delegate to the preceding element 
          */
         if (path != nil
             && (idx = path.indexOf(self)) != nil
             && idx >= 3
             && path[idx - 1] is in (PathIn, PathPeer))
         {
             /* 
              *   we're preceded in the path by a container or peer of ours,
              *   so we know that we're approaching from that "side" -
              *   delegate to that container, since we're coming from that
              *   direction 
              */
             return path[idx - 2].getDropDestination(obj, path);
         }
 
         /* 
          *   We either don't have a path, or we're not preceded in the path
          *   by one of our containers or peers, so we don't have any idea
          *   which "side" we're approaching from.  This means we have no
          *   good basis for deciding where the object being dropped will
          *   fall.  Arbitrarily delegate to our first container, if we have
          *   one.  
          */
         return locationList.length() > 0
             ? locationList[1].getDropDestination(obj, path)
             : nil;
     }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   A "multi-instance" object is a simple way of creating copies of an
  *   object in several places.  This is often useful for decorations and
  *   other features that recur in a whole group of rooms. 
  *   
  *   You define a multi-instance object in two parts.
  *   
  *   First, you define a MultiInstance object, which is just a hollow
  *   shell of an object that sets up the location relationships.  This
  *   shell object doesn't have any presence in the game world; it's just a
  *   programming abstraction.
  *   
  *   Second, as part of the shell object, you define an example of the
  *   object that will actually show up in the game in each of the multiple
  *   locations.  You do this by defining a nested object under the
  *   'instanceObject' property of the shell object.  This is otherwise a
  *   perfectly ordinary object.  In most cases, you'll want to make this a
  *   Decoration, Fixture, or some other non-portable object class, since
  *   the "cloned" nature of these objects means that you usually won't
  *   want them moving around (if they did, you might run into situations
  *   where you had several of them in the same place, leading to
  *   disambiguation headaches for the player).
  *   
  *   Here's an example of how you set up a multi-instance object:
  *   
  *   trees: MultiInstance
  *.    locationList = [forest1, forest2, forest3]
  *.    instanceObject: Fixture { 'tree/trees' 'trees'
  *.      "Many tall, old trees grow here. "
  *.      isPlural = true
  *.    }
  *.  ;
  *   
  *   Note that the instanceObject itself has no location, because it
  *   doesn't appear in the game-world model itself - it's just a template
  *   for the real objects. 
  *   
  *   During initialization, the library will automatically create several
  *   instances (i.e., subclasses) of the example object - one instance per
  *   location, to be exact.  These instances are the real objects that
  *   show up in the game world.
  *   
  *   MultiInstance has one more helpful feature: it lets you dynamically
  *   change the set of locations where the instances appear.  You do this
  *   using the same interface that you use to move around MultiLoc objects
  *   - moveInto(), moveIntoAdd(), moveOutOf().  When you call these
  *   routines on the MultiInstance shell object, it will add and remove
  *   object instances as needed to keep everything consistent.  Thanks to
  *   a little manipulation we do on the instance objects when we set them up,
  *   you can also move the instance objects around directly using
  *   moveInto(), and they'll update the MultiInstance parent to keep its
  *   location list consistent.  
  */
 class MultiInstance: BaseMultiLoc
     /* the template object */
     instanceObject = nil
 
     /* initialize my locations */
     initializeLocation()
     {
         /* create a copy of our template object for each of our locations */
         locationList.forEach({loc: addInstance(loc)});
     }
 
     /* 
      *   Move the MultiInstance into the given location.  This removes us
      *   from any other existing locations and adds us (if we're not
      *   already there) to the given location.  
      */
     moveInto(loc)
     {
         /* remove all instances that aren't in the new location */
         foreach (local cur in instanceList)
         {
             /* if this instance isn't directly in 'loc', remove it */
             if (!cur.isDirectlyIn(loc))
                 cur.moveInto(nil);
         }
 
         /* 
          *   If I don't have an instance object in the new location, add
          *   one.  Since I've dropped every other instance already, we
          *   either have exactly one location now, which is in the new
          *   location, or we have no locations at all; so we need only
          *   check to see if we have any instances and add one in the new
          *   location if not.  
          */
         if (loc != nil && locationList.length() == 0)
             addInstance(loc);
     }
 
     /* 
      *   Add the new location to our set of locations.  Any existing
      *   locations are unaffected. 
      */
     moveIntoAdd(loc)
     {
         /* if I'm not already in the location, add an instance there */
         if (locationList.indexOf(loc) == nil)
             addInstance(loc);
     }
 
     /* 
      *   Remove me from the given location.  Other locations are
      *   unaffected.  
      */
     moveOutOf(loc)
     {
         local inst;
         
         /* find our instance that's in the given location */
         inst = getInstanceIn(loc);
 
         /* if we found it, remove this instance from its location */
         if (inst != nil)
             inst.moveInto(nil);
     }
 
     /* get our instance object (if any) that's in the given location */
     getInstanceIn(loc)
         { return instanceList.valWhich({x: x.isDirectlyIn(loc)}); }
 
     /* internal service routine - add an instance for a given location */
     addInstance(loc)
     {
         local inst;
 
         /* 
          *   Create an instance of the template object, mixing in our
          *   special instance superclass using multiple inheritance.  The
          *   MultiInstanceInstance superclass overrides the location
          *   manipulation methods so that we keep the MultiInstance parent
          *   (i.e., us) synchronized if we move around the instance object
          *   directly (by calling its moveInto() method directly, for
          *   example).  
          */
         inst = TadsObject.createInstanceOf(
             [instanceMixIn, self], [instanceObject]);
 
         /* add it to our list of active instances */
         instanceList.append(inst);
 
         /* move the instance into its new location */
         inst.moveInto(loc);
     }
 
     /* 
      *   If any contents are added to the MultiInstance object, they must
      *   be contents of the template object, so add them to the template
      *   object instead of the MultiInstance parent. 
      */
     addToContents(obj) { instanceObject.addToContents(obj); }
 
     /* 
      *   remove an object from our contents - we'll delegate this to our
      *   template object just like we delegate addToContents 
      */
     removeFromContents(obj) { instanceObject.removeFromContents(obj); }
 
     /* the mix-in superclass for our instance objects */
     instanceMixIn = MultiInstanceInstance
 
     /* our vector of active instance objects */
     instanceList = perInstance(new Vector(5))
 ;
 
 /*
  *   An instance of a MultiInstance object.  This is a mix-in class that
  *   we add (using mutiple inheritance) to each instance.  This overrides
  *   the location manipulation methods, to ensure that we keep the
  *   MultiInstance parent object in sync with any changes made directly to
  *   the instance objects.
  *   
  *   IMPORTANT - the library adds this class to each instance object
  *   *automatically*.  Game code shouldn't ever have to use this class
  *   directly.  
  */
 class MultiInstanceInstance: object
     construct(parent)
     {
         /* remember our MultiInstance parent object */
         miParent = parent;
 
         /* 
          *   clone my contents tree for the new instance, so that we have a
          *   private copy of any components within the instance 
          */
         cloneMultiInstanceContents();
     }
 
     /* move to a new location */
     baseMoveInto(newCont)
     {
         /* 
          *   if we currently have a location, take the location out of our
          *   MultiInstance parent's location list 
          */
         if (location != nil)
             miParent.locationList -= location;
 
         /* inherit the standard behavior */
         inherited(newCont);
 
         /* 
          *   if we have a new location, add the new location to our
          *   MultiInstance parent's location list; otherwise, drop out of
          *   the parent's instance list 
          */
         if (newCont != nil)
         {
             /* 
              *   add the new location to the parent's location list, if
              *   we're not already there 
              */
             if (miParent.locationList.indexOf(newCont) == nil)
                 miParent.locationList += newCont;
         }
         else
         {
             /* 
              *   we're being removed from the game world, so remove this
              *   instance from the parent's instance list 
              */
             miParent.instanceList.removeElement(self);
         }
     }
 
     /* 
      *   All instances of a given MultiInstance are equivalent to one
      *   another, for parsing purposes.  
      */
     isEquivalent = true
 
     /* our MultiInstance parent */
     miParent = nil
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   A "multi-faceted" object is similar to a MultiInstance object, with
  *   the addition that the instance objects are "facets" of one another.
  *   This means that they have the same identity, from the perspective of
  *   a character in the scenario: all of the instance objects are part of
  *   the same conceptual object, not separate objects.
  *   
  *   This is especially useful for large objects that span multiple
  *   locations, such as a river or a long rope.
  *   
  *   You define a multi-faceted object the same way you set up a
  *   MultiInstance: definfe a MultiFaceted shell object, and as part of
  *   the shell, define the facet object using the instanceObject property.
  *   Here's an example:
  *   
  *   river: MultiFaceted
  *.    locationList = [riverBank, meadow, canyon]
  *.    instanceObject: Fixture { 'river' 'river'
  *.      "The river meanders by. "
  *.    }
  *.  ;
  *   
  *   The main difference between MultiInstance and MultiFaceted is that
  *   the "facet" objects of a MultiFaceted are related as facets of a
  *   common object from the parser's perspective.  For example, if a
  *   player refers to one facet, then travels to another location that
  *   contains a different facet, then refers to "it", the parser will
  *   realize that the pronoun refers to the new facet in the new location.
  */
 class MultiFaceted: MultiInstance
     /* our instance objects represent our facets for parsing purposes */
     getFacets() { return instanceList; }
 
     /* the mix-in superclass for our instance objects */
     instanceMixIn = MultiFacetedFacet
 ;
 
 /* 
  *   The mix-in superclass for MultiFaceted facet instances.
  *   
  *   IMPORTANT - the library adds this class to each instance object
  *   *automatically*.  Game code shouldn't ever have to use this class
  *   directly.  
  */
 class MultiFacetedFacet: MultiInstanceInstance
     /* 
      *   Get our other facets for parsing purposes - our parent maintains
      *   the list of all of its facets, so simply return that list.  (Note
      *   that we'll be in the list as well, but that's harmless, so don't
      *   bother removing us.) 
      */
     getFacets() { return miParent.getFacets(); }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   A "linkable" object is one that can participate in a master/slave
  *   relationship.  This kind of relationship means that the state of both
  *   objects in the pair is controlled by one of the objects, called the
  *   master; the other object defers to the other to get and set all of
  *   its linkable state.
  *   
  *   Note that this base class doesn't provide for the management of any
  *   of the actual linked state.  Subclasses are responsible for doing
  *   this.  The general pattern is to create a getter/setter method pair
  *   for each bit of linked state, and in these methods refer to
  *   masterObject.xxx rather than just self.xxx.
  *   
  *   This is useful for objects such as doors that have two separate
  *   objects representing the two sides of the door.  The two sides are
  *   always linked for things like open/closed and locked/unlocked state;
  *   this can be handled by linking the two sides, and managing all state
  *   of both sides in one side designated as the master.  
  */
 class Linkable: object
     /*
      *   Get the master object, which holds our state.  By default, this
      *   is simply 'self', but some objects might want to override this.
      *   For example, doors are usually implemented with two separate
      *   objects, representing the two sides of the door, which share
      *   common state; in such cases, one of the pair can be designated as
      *   the master, which holds the common state of the door, and this
      *   method can be overridden so that all state operations on the lock
      *   are performed on the master side of the door.
      *   
      *   We return self by default so that a linkable object can stand
      *   alone if desired.  That is, a linkable object doesn't have to be
      *   part of a pair; it can just as well be a single object.  
      */
     masterObject()
     {
         /* 
          *   inherit from the next superclass, if possible; otherwise, use
          *   'self' as the default master object 
          */
         if (canInherit())
             return inherited();
         else
             return self;
     }
 
     /*
      *   We're normally mixed into a Thing; do some extra work in
      *   initialization. 
      */
     initializeThing()
     {
         /* inherit the default handling */
         inherited();
 
         /* 
          *   If we're tied to a separate master object, check the master
          *   object to see if it's tied back to us as its master object.
          *   Only one can be the master; if each says the other is the
          *   master, we'll get stuck in infinite loops as each tries to
          *   defer to the other.  To avoid this, break the loop by
          *   arbitrarily choosing one or the other as the master.  Note
          *   that we don't have to worry about the other object making a
          *   different decision and breaking the relationship, because if
          *   we detect the loop, it means we're going first - if the other
          *   object had gone first then it would have detected and broken
          *   the loop itself, and we wouldn't be finding the loop now.
          */
         if (masterObject != self && masterObject.masterObject == self)
         {
             /* 
              *   We're tied together in a loop - break the loop by
              *   arbitrarily electing myself as the master object.
              *   Because these relationships are symmetric, it shouldn't
              *   matter which we choose.  
              */
             masterObject = self;
         }
     }
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   A "basic openable" is an object that keeps open/closed status, and
  *   which can be linked to another object to maintain that status.  This
  *   basic class doesn't handle any special commands; it's purely for
  *   keeping track of internal open/closed state.  
  */
 class BasicOpenable: Linkable
     /* 
      *   Initial open/closed setting.  Set this to true to make the object
      *   open initially.  If this object is linked to another object (as
      *   in the two sides of a door), you only need to set this property
      *   in the *master* object - the other side will automatically link
      *   up to the master object during initialization.  
      */
     initiallyOpen = nil
 
     /*
      *   Flag: door is open.  Travel is only possible when the door is
      *   open.  Return the master's status.
      */
     isOpen()
     {
         /* 
          *   If we're the master, simply use our isOpen_ property;
          *   otherwise, call our master's isOpen method.  This way, if the
          *   master has a different way of calculating isOpen, we'll defer
          *   to its different handling. 
          */
         if (masterObject == self)
             return isOpen_;
         else
             return masterObject.isOpen();
     }
 
     /*
      *   Make the object open or closed.  By default, we'll simply set the
      *   isOpen flag to the new status.  Objects can override this to
      *   apply side effects of opening or closing the object.  
      */
     makeOpen(stat)
     {
         /* 
          *   if we're the master, simply set our isOpen_ property;
          *   otherwise, defer to the master 
          */
         if (masterObject == self)
             isOpen_ = stat;
         else
             masterObject.makeOpen(stat);
 
         /* inherit the next superclass's handling */
         inherited(stat);
     }
 
     /*
      *   Open status name.  This is an adjective describing whether the
      *   object is opened or closed.  In English, this will return "open"
      *   or "closed."  
      */
     openDesc = (isOpen ? gLibMessages.openMsg(self)
                        : gLibMessages.closedMsg(self))
 
     /* initialization */
     initializeThing()
     {
         /* inherit the default handling */
         inherited();
 
         /* if we're the master, set our initial open/closed state */
         if (masterObject == self)
             isOpen_ = initiallyOpen;
     }
 
     /*
      *   If we're obstructing a sense path, it must be because we're
      *   closed.  Try implicitly opening.  
      */
     tryImplicitRemoveObstructor(sense, obj)
     {
         /* 
          *   try opening me, returning true if we attempt the command, nil
          *   if not 
          */
         return tryImplicitAction(Open, self);
     }
 
     /* 
      *   if we can't reach or move something through the container, it
      *   must be because we're closed 
      */
     cannotTouchThroughMsg = &cannotTouchThroughClosedMsg
     cannotMoveThroughMsg = &cannotMoveThroughClosedMsg
 
     /* 
      *   Internal open/closed status.  Do not use this for initialization
      *   - set initiallyOpen in the master object instead. 
      */
     isOpen_ = nil
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Openable: a mix-in class that can be combined with an object's other
  *   superclasses to make the object respond to the verbs "open" and
  *   "close."  We also add some extra features for other related verbs,
  *   such as a must-be-open precondition "look in" and "board".  
  */
 class Openable: BasicOpenable
     /*
      *   Describe our contents using a special version of the contents
      *   lister, so that we add our open/closed status to the listing.  The
      *   message we add is given by our openStatus method, so if all you
      *   want to change is the "it's open" status message, you can just
      *   override openStatus rather than providing a whole new lister.  
      */
     descContentsLister = openableContentsLister
 
     /*
      *   Contents lister to use when we're opening the object.  This
      *   lister shows the items that are newly revealed when the object is
      *   opened. 
      */
     openingLister = openableOpeningLister
 
     /* 
      *   Get our "open status" message - this is a complete sentence saying
      *   that we're open or closed.  By default, in English, we just say
      *   "it's open" (adjusted for number and gender, of course).
      *   
      *   Note that this message has to be a stand-alone independent clause.
      *   In particular note that we don't put any spacing after it, since
      *   we need to be able to add sentence-ending or clause-ending
      *   punctuation immediately after it.  
      */
     openStatus() { return gLibMessages.openStatusMsg(self); }
 
     /*
      *   By default, an Openable that's also a Lockable must be closed to
      *   be locked.  This means that when it's open, the object is
      *   implicitly unlocked, in which case "It's unlocked" isn't worth
      *   mentioning when the description says "It's open."
      */
     lockStatusReportable = (!isOpen)
 
     /*
      *   Action handlers 
      */
     dobjFor(Open)
     {
         verify()
         {
             /* it makes no sense to open something that's already open */
             if (isOpen)
                 illogicalAlready(&alreadyOpenMsg);
         }
         action()
         {
             local trans;
             
             /* 
              *   note the effect we have currently, while still closed, on
              *   sensing from outside into our contents 
              */
             trans = transSensingIn(sight);
             
             /* make it open */
             makeOpen(true);
 
             /* 
              *   make the default report - if we make a non-default
              *   report, the default will be ignored, so we don't need to
              *   worry about whether or not we'll make a non-default
              *   report now 
              */
             defaultReport(&okayOpenMsg);
 
             /*
              *   If the actor is outside me, and we have any listable
              *   contents, and our sight transparency is now better than it
              *   was before we were open, reveal the new contents.
              *   Otherwise, just show our default 'opened' message.
              *   
              *   As a special case, if we're running as an implied command
              *   within a LookIn or Search action on this same object,
              *   don't bother showing this result.  Doing so would be
              *   redundant with the explicit examination of the contents
              *   that we'll be doing anyway with the main action.  
              */
             if (!gActor.isIn(self)
                 && transparencyCompare(transSensingIn(sight), trans) > 0
                 && !(gAction.isImplicit
                      && (gAction.parentAction.ofKind(LookInAction)
                          || gAction.parentAction.ofKind(SearchAction))
                      && gAction.parentAction.getDobj() == self))
             {
                 local tab;
 
                 /* get the table of visible objects */
                 tab = gActor.visibleInfoTable();
                 
                 /* show my contents list, if I have any */
                 openingLister.showList(gActor, self, contents, ListRecurse,
                                        0, tab, nil);
 
                 /* mark my contents as having been seen */
                 setContentsSeenBy(tab, gActor);
 
                 /* show any special contents as well */
                 examineSpecialContents();
             }
         }
     }
 
     dobjFor(Close)
     {
         verify()
         {
             /* it makes no sense to close something that's already closed */
             if (!isOpen)
                 illogicalAlready(&alreadyClosedMsg);
         }
         action()
         {
             /* make it closed */
             makeOpen(nil);
 
             /* show the default report */
             defaultReport(&okayCloseMsg);
         }
     }
 
     dobjFor(LookIn)
     {
         /* 
          *   to look in an openable object, we must be open, unless the
          *   object is transparent or the actor is inside us 
          */
         preCond
         {
             local lst;
 
             /* get the inherited preconditions */
             lst = nilToList(inherited());
 
             /* 
              *   if I'm not transparent looking in, and the actor isn't
              *   already inside me, try opening me 
              */
             if (transSensingIn(sight) != transparent && !gActor.isIn(self))
                 lst += objOpen;
 
             /* return the result */
             return lst;
         }
     }
 
     dobjFor(Search)
     {
         /* 
          *   To search an openable object, we must be open - unlike LOOK
          *   IN, this applies even if the object is transparent, since
          *   SEARCH is inherently more aggressive than LOOK IN, and implies
          *   physically picking through the contents.  This doesn't apply
          *   if the actor is already inside me.  
          */
         preCond
         {
             /* get the inherited preconditions */
             local lst = nilToList(inherited());
 
             /* if the actor isn't in me, make sure I'm open */
             if (!gActor.isIn(self))
                 lst += objOpen;
 
             /* return the updated list */
             return lst;
         }
     }
 
     /*
      *   Generate a precondition to make sure gActor can reach the interior
      *   of the container.  We consider the inside reachable if either the
      *   actor is located inside the container, or the actor is outside and
      *   the container is open.  
      */
     addInteriorReachableCond(lst)
     {
         /* 
          *   If the actor's inside us, they can reach our interior whether
          *   we're open or not, so there's no need for any additional
          *   condition.  If not, we need to be open for the actor to be
          *   able to reach our interior.  
          */
         if (!gActor.isIn(self))
             lst = nilToList(lst) + objOpen;
 
         /* return the result */
         return lst;
     }
 
     iobjFor(PutIn)
     {
         /* make sure that our interior is reachable */
         preCond { return addInteriorReachableCond(inherited()); }
     }
 
     iobjFor(PourInto)
     {
         /* make sure that our interior is reachable */
         preCond { return addInteriorReachableCond(inherited()); }
     }
 
     /* can't lock an openable that isn't closed */
     dobjFor(Lock)
     {
         preCond { return nilToList(inherited()) + objClosed; }
     }
     dobjFor(LockWith)
     {
         preCond { return nilToList(inherited()) + objClosed; }
     }
 
     /* must be open to get out of a nested room */
     dobjFor(GetOutOf)
     {
         preCond()
         {
             return nilToList(inherited())
                 + new ObjectPreCondition(self, objOpen);
         }
     }
 
     /* must be open to get into a nested room */
     dobjFor(Board)
     {
         preCond()
         {
             return nilToList(inherited())
                 + new ObjectPreCondition(self, objOpen);
         }
     }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Lockable: a mix-in class that can be combined with an object's other
  *   superclasses to make the object respond to the verbs "lock" and
  *   "unlock."  A Lockable requires no key.
  *   
  *   Note that Lockable should usually go BEFORE a Thing-derived class in
  *   the superclass list.  
  */
 class Lockable: Linkable
     /* 
      *   Our initial locked state (i.e., at the start of the game).  By
      *   default, we start out locked. 
      */
     initiallyLocked = true
 
     /*
      *   Current locked state.  Use our isLocked_ status if we're the
      *   master, otherwise defer to the master.  
      */
     isLocked()
     {
         if (masterObject == self)
             return isLocked_;
         else
             return masterObject.isLocked();
     }
 
     /*
      *   Make the object locked or unlocked.  Objects can override this to
      *   apply side effects of locking or unlocking.  By default, if we're
      *   the master, we'll simply set our isLocked_ property to the new
      *   status, and otherwise defer to the master object.  
      */
     makeLocked(stat)
     {
         /* apply to self or the master object, as appropriate */
         if (masterObject == self)
             isLocked_ = stat;
         else
             masterObject.makeLocked(stat);
 
         /* inherit the next superclass's handling */
         inherited();
     }
 
     /* show our status */
     examineStatus()
     {
         /* inherit the default handling */
         inherited();
 
         /* 
          *   if our lock status is visually apparent, and we want to
          *   mention the lock status in our current state, show the lock
          *   status 
          */
         if (lockStatusObvious && lockStatusReportable)
             say(isLocked ? gLibMessages.currentlyLocked
                          : gLibMessages.currentlyUnlocked);
     }
 
     /* description of the object as locked or unlocked */
     lockedDesc = (isLocked() ? gLibMessages.lockedMsg(self)
                              : gLibMessages.unlockedMsg(self))
 
     /*
      *   Is our 'locked' status obvious?  This should be set to true for an
      *   object whose locked/unlocked status can be visually observed, nil
      *   for an object whose status is not visuall apparent.  For example,
      *   you can usually tell from the inside that a door is locked by
      *   looking at the position of the lock's paddle, but on the outside
      *   of a door there's usually no way to see the status.
      *   
      *   By default, since we can be locked and unlocked with simple LOCK
      *   and UNLOCK commands, we assume the status is as obvious as the
      *   mechanism must be to allow such simple commands.  
      */
     lockStatusObvious = true
 
     /*
      *   Is our 'locked' status reportable in our current state?  This is
      *   similar to lockStatusObvious, but serves a separate purpose: this
      *   tells us if we wish to report the lock status for aesthetic
      *   reasons.
      *   
      *   This property is primarily of interest to mix-ins.  To allow
      *   mix-ins to get a say, regardless of the order of superclasses,
      *   we'll by default defer to any inherited value if there is in fact
      *   an inherited value.  If there's no inherited value, we'll simply
      *   return true.
      *   
      *   We use this in the library for one case in particular: when we're
      *   mixed with Openable, we don't want to report the lock status for
      *   an open object because an Openable must by default be closed to be
      *   locked.  That is, when an Openable is open, it's always unlocked,
      *   so reporting that it's unlocked is essentially redundant
      *   information.  
      */
     lockStatusReportable = (canInherit() ? inherited() : true)
 
     /* 
      *   Internal locked state.  Do not use this to set the initial state
      *   - set initiallyLocked in the master object instead. 
      */
     isLocked_ = nil
     
     /* initialization */
     initializeThing()
     {
         /* inherit the default handling */
         inherited();
         
         /* if we're the master, set our initial state */
         if (masterObject == self)
             isLocked_ = initiallyLocked;
     }
 
     /*
      *   Action handling 
      */
 
     /* "lock" */
     dobjFor(Lock)
     {
         preCond = (nilToList(inherited()) + [touchObj])
         verify()
         {
             /* if we're already locked, there's no point in locking us */
             if (isLocked)
                 illogicalAlready(&alreadyLockedMsg);
         }
         action()
         {
             /* make it locked */
             makeLocked(true);
 
             /* make the default report */
             defaultReport(&okayLockMsg);
         }
     }
 
     /* "unlock" */
     dobjFor(Unlock)
     {
         preCond = (nilToList(inherited()) + [touchObj])
         verify()
         {
             /* if we're already unlocked, there's no point in doing this */
             if (!isLocked)
                 illogicalAlready(&alreadyUnlockedMsg);
         }
         action()
         {
             /* make it unlocked */
             makeLocked(nil);
 
             /* make the default report */
             defaultReport(&okayUnlockMsg);
         }
     }
 
     /* "lock with" */
     dobjFor(LockWith)
     {
         preCond = (nilToList(inherited()) + [touchObj])
         verify() { illogical(&noKeyNeededMsg); }
     }
 
     /* "unlock with" */
     dobjFor(UnlockWith)
     {
         preCond = (nilToList(inherited()) + [touchObj])
         verify() { illogical(&noKeyNeededMsg); }
     }
 
     /*
      *   Should we automatically unlock this door on OPEN?  By default, we
      *   do this only if the lock status is obvious.  
      */
     autoUnlockOnOpen = (lockStatusObvious)
     
     /* 
      *   A locked object can't be opened - apply a precondition and a check
      *   for "open" that ensures that we unlock this object before we can
      *   open it.
      *   
      *   If the lock status isn't obvious, don't try to unlock the object
      *   as a precondition.  Instead, test to make sure it's unlocked in
      *   the 'check' routine, and fail.  
      */
     dobjFor(Open)
     {
         preCond()
         {
             /* start with the inherited preconditions */
             local ret = nilToList(inherited());
 
             /* automatically unlock on open, if appropriate */
             if (autoUnlockOnOpen)
                 ret += objUnlocked;
 
             /* return the result */
             return ret;
         }
 
         check()
         {
             /* make sure we're unlocked */
             if (isLocked)
             {
                 /* let them know we're locked */
                 reportFailure(&cannotOpenLockedMsg);
 
                 /* set 'it' to me, so UNLOCK IT works */
                 gActor.setPronounObj(self);
 
                 /* we cannot proceed */
                 exit;
             }
         }
     }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   A lockable that can't be locked and unlocked by direct action.  The
  *   LOCK and UNLOCK commands cannot be used with this kind of lockable.
  *   
  *   This is useful for a couple of situations.  First, it's useful when we
  *   want to create a locked object that simply can't be unlocked, such as
  *   a locked door that forms a permanent boundary of the map.  Second,
  *   it's useful for locked objects that must be unlocked by some other
  *   means, such as manipulating an external mechanism (pulling a lever,
  *   say).  In these cases, the trick is to figure out the separate means
  *   of unlocking the door, so we don't want the LOCK and UNLOCK commands
  *   to work directly.  
  */
 class IndirectLockable: Lockable
     dobjFor(Lock)
     {
         check()
         {
             reportFailure(cannotLockMsg);
             exit;
         }
     }
     dobjFor(LockWith) asDobjFor(Lock)
     dobjFor(Unlock)
     {
         check()
         {
             reportFailure(cannotUnlockMsg);
             exit;
         }
     }
     dobjFor(UnlockWith) asDobjFor(Unlock)
 
     /*
      *   Since we can't be locked and unlocked with simple LOCK and UNLOCK
      *   commands, presume that the lock status isn't obvious.  If the
      *   alternative mechanism that locks and unlocks the object makes the
      *   current status readily apparent, this should be overridden and set
      *   to true.  
      */
     lockStatusObvious = nil
 
     /* the message we display in response to LOCK/UNLOCK */
     cannotLockMsg = &unknownHowToLockMsg
     cannotUnlockMsg = &unknownHowToUnlockMsg
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   LockableWithKey: a mix-in class that can be combined with an object's
  *   other superclasses to make the object respond to the verbs "lock" and
  *   "unlock," with a key as an indirect object.  A LockableWithKey cannot
  *   be locked or unlocked except with the keys listed in the keyList
  *   property.
  *   
  *   Note that LockableWithKey should usually go BEFORE a Thing-derived
  *   class in the superclass list.  
  */
 class LockableWithKey: Lockable
     /*
      *   Determine if the key fits this lock.  Returns true if so, nil if
      *   not.  By default, we'll return true if the key is in my keyList.
      *   This can be overridden to use other key selection criteria.  
      */
     keyFitsLock(key) { return keyList.indexOf(key) != nil; }
 
     /*
      *   Determine if the key is plausibly of the right type for this
      *   lock.  This doesn't check to see if the key actually fits the
      *   lock - rather, this checks to see if the key is generally the
      *   kind of object that might plausibly be used with this lock.
      *   
      *   The point of this routine is to make this class concerned only
      *   with the abstract notion of objects that serve to lock and unlock
      *   other objects, without requiring that the key objects resemble
      *   little notched metal sticks or that the lock objects resemble
      *   cylinders with pins - or, more specifically, without requiring
      *   that all of the kinds of keys in a game remotely resemble one
      *   another.
      *   
      *   For example, one kind of "key" in a game might be a plastic card
      *   with a magnetic stripe, and the corresponding lock would be a
      *   card slot; another kind of key might the traditional notched
      *   metal stick.  Clearly, no one would ever think to use a plastic
      *   card with a conventional door lock, nor would one try to put a
      *   house key into a card slot (not with the expectation that it
      *   would actually work, anyway).  This routine is meant to
      *   facilitate this kind of distinction: the card slot can use this
      *   routine to indicate that only plastic card objects are plausible
      *   as keys, and door locks can indicate that only metal keys are
      *   plausible.
      *   
      *   This routine can be used for disambiguation and other purposes
      *   when we must programmatically select a key that is not specified
      *   or is only vaguely specified.  For example, the keyring searcher
      *   uses it so that, when we're searching for a key on a keyring to
      *   open this lock, we implicitly try only the kinds of keys that
      *   would be plausibly useful for this kind of lock.
      *   
      *   By default, we'll simply return true.  Subclasses specific to a
      *   game (such as the "card reader" base class or the "door lock"
      *   base class) can override this to discriminate among the
      *   game-specific key classes.  
      */
     keyIsPlausible(key) { return true; }
 
     /* the list of objects that can serve as keys for this object */
     keyList = []
 
     /* 
      *   The list of keys which the player knows will fit this lock.  This
      *   is used to make key disambiguation automatic once the player
      *   knows the correct key for a lock.  
      */
     knownKeyList = []
 
     /* 
      *   Get my known key list.  This simply returns the known key list
      *   from the known key owner. 
      */
     getKnownKeyList() { return getKnownKeyOwner().knownKeyList; }
 
     /* 
      *   Get the object that own our known key list.  If we explicitly have
      *   our own non-empty known key list, we own the key list; otherwise,
      *   our master object owns the list, as long as it has a non-nil key
      *   list at all.  
      */
     getKnownKeyOwner()
     {
         /* 
          *   if we have a non-empty key list, or our master object doesn't
          *   have a key list at all, use our list; otherwise, use our
          *   master object's list so use our list 
          */
         if (knownKeyList.length() != 0 || masterObject.knownKeyList == nil)
             return self;
         else
             return masterObject;
     }
 
     /*
      *   Flag: remember my keys after they're successfully used.  If this
      *   is true, whenever a key is successfully used to lock or unlock
      *   this object, we'll add the key to our known key list;
      *   subsequently, whenever we try to use a key in this lock, we will
      *   automatically disambiguate the key based on the keys known to
      *   work previously.
      *   
      *   Some authors might prefer not to assume that the player should
      *   remember which keys operate which locks, so this property can be
      *   changed to nil to eliminate this memory feature.  By default we
      *   set this to true, since it shouldn't generally give away any
      *   secrets or puzzles for the game to assume that a key that was
      *   used successfully once with a given lock is the one to be used
      *   subsequently with the same lock.  
      */
     rememberKnownKeys = true
 
     /*
      *   Determine if the player knows that the given key operates this
      *   lock.  Returns true if the key is in our known key list, nil if
      *   not.  
      */
     isKeyKnown(key) { return getKnownKeyList().indexOf(key) != nil; }
 
     /* 
      *   By default, the locked/unlocked status of a keyed lockable is nil.
      *   In most cases, an object that's locked and unlocked using a key
      *   doesn't have a visible indication of the status; for example, you
      *   usually can't tell just by looking at it from the outside whether
      *   or not an exterior door to a building is locked.  Usually, the
      *   only way to tell from the outside that an exterior door is locked
      *   is to try opening it and see if it opens.  
      */
     lockStatusObvious = nil
 
     /*
      *   Should we automatically unlock on OPEN?  We will if our inherited
      *   handling says so, OR if the current actor is carrying a key
      *   that's known to work with this object.  We automatically unlock
      *   when a known key is present as a convenience: if we have a known
      *   key, then there's no mystery in unlocking this object, and thus
      *   for playability we want to make its operation fully automatic.  
      */
     autoUnlockOnOpen()
     {
         return (inherited()
                 || getKnownKeyList.indexWhich({x: x.isIn(gActor)}) != nil);
     }
 
     /*
      *   Action handling 
      */
 
     dobjFor(Lock)
     {
         preCond
         {
             /* 
              *   remove any objClosed from our precondition - since we
              *   won't actually do any locking but will instead merely ask
              *   for an indirect object, we don't want to apply the normal
              *   closed precondition here 
              */
             return inherited() - objClosed;
         }
         verify()
         {
             /* if we're already locked, there's no point in locking us */
             if (isLocked)
                 illogicalAlready(&alreadyLockedMsg);
         }
         action()
         {
             /* ask for an indirect object to use as the key */
             askForIobj(LockWith);
         }
     }
 
     /* "unlock" */
     dobjFor(Unlock)
     {
         verify()
         {
             /* if we're not locked, there's no point in unlocking us */
             if (!isLocked)
                 illogicalAlready(&alreadyUnlockedMsg);
         }
         action()
         {
             /*
              *   We need a key.  If we're running as an implied action, the
              *   player hasn't specifically proposed unlocking the object,
              *   so it's a little weird to ask a follow-up question about
              *   what key to use.  So, if the action is implicit and
              *   there's no default key, don't proceed; simply fail with an
              *   explanation.  
              */
             if (gAction.isImplicit
                 && !UnlockWithAction.testRetryDefaultIobj(gAction))
             {
                 /* explain that we need a key, and we're done */
                 reportFailure(&unlockRequiresKeyMsg);
                 return;
             }
             
             /* ask for a key */
             askForIobj(UnlockWith);
         }
     }
 
     /* 
      *   perform the action processing for LockWith or UnlockWith - these
      *   are highly symmetrical, in that the only thing that varies is the
      *   new lock state we establish 
      */
     lockOrUnlockAction(lock)
     {
         /* 
          *   If it's a keyring, let the keyring's action handler do the
          *   work.  Otherwise, if it's my key, lock/unlock; it's not a
          *   key, fail.  
          */
         if (gIobj.ofKind(Keyring))
         {
             /* 
              *   do nothing - let the indirect object action handler do
              *   the work 
              */
         }
         else if (keyFitsLock(gIobj))
         {
             local ko;
 
             /* 
              *   get the object (us or our master object) that owns the
              *   known key list 
              */
             ko = getKnownKeyOwner();
             
             /* 
              *   if the key owner remembers known keys, and it doesn't know
              *   about this working key yet, remember this in the list of
              *   known keys 
              */
             if (ko.rememberKnownKeys
                 && ko.knownKeyList.indexOf(gIobj) == nil)
                 ko.knownKeyList += gIobj;
             
             /* set my new state and issue a default report */
             makeLocked(lock);
             defaultReport(lock ? &okayLockMsg : &okayUnlockMsg);
         }
         else
         {
             /* the key doesn't work in this lock */
             reportFailure(&keyDoesNotFitLockMsg);
         }
     }
 
     /* "lock with" */
     dobjFor(LockWith)
     {
         verify()
         {
             /* if we're already locked, there's no point in locking us */
             if (isLocked)
                 illogicalAlready(&alreadyLockedMsg);
         }
         action()
         {
             /* perform the generic lock/unlock action processing */
             lockOrUnlockAction(true);
         }
     }
 
     /* "unlock with" */
     dobjFor(UnlockWith)
     {
         verify()
         {
             /* if we're not locked, there's no point in unlocking us */
             if (!isLocked)
                 illogicalAlready(&alreadyUnlockedMsg);
         }
         action()
         {
             /* perform the generic lock/unlock action processing */
             lockOrUnlockAction(nil);
         }
     }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   The common base class for containers and surfaces: things that have
  *   limited bulk capacities.  This class isn't usually used directly;
  *   subclasses such as Surface and Container are usually used instead.  
  */
 class BulkLimiter: Thing
     /*
      *   A container can limit the cumulative amount of bulk of its
      *   contents, and the maximum bulk of any one object, using
      *   bulkCapacity and maxSingleBulk.  We count the cumulative and
      *   single-item limits separately, since we want to allow modelling
      *   some objects as so large that they won't fit in this container at
      *   all, even if the container is carrying nothing else, without
      *   limiting the number of small items we can carry.
      *   
      *   By default, we set bulkCapacity to a very large number, making
      *   the total capacity of the object essentially unlimited.  However,
      *   we set maxSingleBulk to a relatively low number - this way, if an
      *   author wants to designate certain objects as especially large and
      *   thus unable to fit in ordinary containers, the author merely
      *   needs to set the bulk of those large items to something greater
      *   than 10.  On the other hand, if an author doesn't want to worry
      *   about bulk and limited carrying capacities and simply uses
      *   library defaults for everything, we will be able to contain
      *   anything and everything.
      *   
      *   In a game that models bulk realistically, a container's bulk
      *   should generally be equal to or slightly greater than its
      *   bulkCapacity, because a container shouldn't be smaller on the
      *   outside than on the inside.  If bulkCapacity exceeds bulk, the
      *   player can work around a holding bulk limit by piling objects
      *   into the container, thus "hiding" the bulks of the contents
      *   behind the smaller bulk of the container.  
      */
     bulkCapacity = 10000
     maxSingleBulk = 10
 
     /* 
      *   receive notification that we're about to insert an object into
      *   this container 
      */
     notifyInsert(obj, newCont)
     {
         /* if I'm the new direct container, check our bulk limit */
         if (newCont == self)
         {
             /* 
              *   do a 'what if' test to see what would happen to our
              *   contained bulk if we moved this item into me 
              */
             obj.whatIf({: checkBulkInserted(obj)}, &moveInto, self);
         }
 
         /* inherit base class handling */
         inherited(obj, newCont);
     }
 
     /*
      *   Check to see if a proposed insertion - already tentatively in
      *   effect when this routine is called - would overflow our bulk
      *   limits.  Reports failure and exits if the inserted object would
      *   exceed our capacity. 
      */
     checkBulkInserted(insertedObj)
     {
         local objBulk;
 
         /* get the bulk of the inserted object itself */
         objBulk = insertedObj.getBulk();
 
         /*
          *   Check the object itself to see if it fits by itself.  If it
          *   doesn't, we can report the simple fact that the object is too
          *   big for the container.  
          */
         if (objBulk > maxSingleBulk || objBulk > bulkCapacity)
         {
             reportFailure(&tooLargeForContainerMsg, insertedObj, self);
             exit;
         }
             
         /* 
          *   If our contained bulk is over our maximum, don't allow it.
          *   Note that we merely need to check our current bulk within,
          *   since this routine is called with the insertion already
          *   tentatively in effect. 
          */
         if (getBulkWithin() > bulkCapacity)
         {
             reportFailure(tooFullMsg, insertedObj, self);
             exit;
         }
     }
 
     /* 
      *   the message property to use when we're too full to hold a new
      *   object (i.e., the object's bulk would push us over our bulk
      *   capacity limit) 
      */
     tooFullMsg = &containerTooFullMsg
 
     /* 
      *   the message property to use when doing something to one of our
      *   contents would make it too large to fit all by itself into this
      *   container (that is, it would cause that object's bulk to exceed
      *   our maxSingleBulk) 
      */
     becomingTooLargeMsg = &becomingTooLargeForContainerMsg
 
     /* 
      *   the message property to use when doing something to one of our
      *   contents would cause our overall contents to exceed our capacity 
      */
     becomingTooFullMsg = &containerBecomingTooFullMsg
 
     /*
      *   Check a bulk change of one of my direct contents. 
      */
     checkBulkChangeWithin(obj)
     {
         local objBulk;
         
         /* get the object's new bulk */
         objBulk = obj.getBulk();
         
         /* 
          *   if this change would cause the object to exceed our
          *   single-item bulk limit, don't allow it 
          */
         if (objBulk > maxSingleBulk || objBulk > bulkCapacity)
         {
             reportFailure(becomingTooLargeMsg, obj, self);
             exit;
         }
 
         /* 
          *   If our total carrying capacity is exceeded with this change,
          *   don't allow it.  Note that 'obj' is already among our
          *   contents when this routine is called, so we can simply check
          *   our current total bulk within.  
          */
         if (getBulkWithin() > bulkCapacity)
         {
             reportFailure(becomingTooFullMsg, obj, self);
             exit;
         }
     }
 
     /*
      *   Adjust a THROW destination.  Since we only allow a limited amount
      *   of bulk within our contents, we need to make sure the thrown
      *   object would fit if it landed here.  If it doesn't, we'll redirect
      *   the landing site to our container.  
      */
     adjustThrowDestination(thrownObj, path)
     {
         local thrownBulk = thrownObj.getBulk();
         local newBulk;
         local dest;
 
         /* 
          *   do a 'what if' test to test our total bulk with the projectile
          *   added to my contents 
          */
         newBulk = thrownObj.whatIf({: getBulkWithin()}, &moveInto, self);
 
         /* 
          *   If that exceeds our maximum bulk, or the object's bulk
          *   individually is over our limit, we can't be the landing site.
          *   In this case, defer to our location's drop destination, if it
          *   has one.  
          */
         if ((newBulk > bulkCapacity
             || thrownBulk > bulkCapacity
             || thrownBulk > maxSingleBulk)
             && location != nil
             && (dest = location.getDropDestination(thrownObj, path)) != nil)
         {
             /* 
              *   It won't fit, so defer to our container's drop
              *   destination.  Give the new destination a chance to further
              *   adjust the destination.  
              */
             return dest.adjustThrowDestination(thrownObj, path);
         }
 
         /* 
          *   the projectile fits, or we just can't find a container to
          *   defer to; use the original destination, i.e., self 
          */
         return self;
     }
 
     /*
      *   Examine my interior.  This can be used to handle the action() for
      *   LOOK IN, or for other commands appropriate to the subclass.  
      */
     examineInterior()
     {
         /* examine the interior with our normal look-in lister */
         examineInteriorWithLister(lookInLister);
 
         /* 
          *   Anything that the an overriding caller (a routine that called
          *   us with 'inherited') wants to add is an addendum to our
          *   description, so add a transcript marker to indicate tht the
          *   main description is now finished.
          *   
          *   The important thing about this is that any message that an
          *   overriding caller wants to add is not considered part of the
          *   description, in the sense that we don't want it to suppress
          *   any default description we've already generated.  One of the
          *   transformations we apply to the transcript is to suppress any
          *   default descriptive text if there's any more specific
          *   descriptive text following (for example, we suppress "It's an
          *   ordinary <thing>" if we also are going to say "it's open" or
          *   "it contains three coins").  If we have an overriding caller
          *   who's going to add anything, then we must assume that what
          *   the caller's adding is something about the act of examining
          *   the object, rather than a description of the object, so we
          *   don't want it to suppress a default description.  
          */
         gTranscript.endDescription();
     }
 
     /* examine my interior, listing the contents with the given lister */
     examineInteriorWithLister(lister)
     {
         local tab;
 
         /* if desired, reveal any "Hidden" items concealed within */
         if (revealHiddenItems)
         {
             /* scan our contents and reveal each Hidden item */
             foreach (local cur in contents)
             {
                 /* if it's a Hidden item, reveal it */
                 if (cur.ofKind(Hidden))
                     cur.discover();
             }
         }
 
         /* get my visible sense info */
         tab = gActor.visibleInfoTable();
             
         /* show my contents, if I have any */
         lister.showList(gActor, self, contents, ListRecurse,  0, tab, nil);
 
         /* mark my contents as having been seen */
         setContentsSeenBy(tab, gActor);
 
         /* examine my special contents */
         examineSpecialContents();
     }
 
     /*
      *   Verify putting something new in my interior.  This is suitable
      *   for use as a verify() method for a command like PutIn or PutOn.
      *   Note that this routine assumes and requires that gDobj be the
      *   object to be added, and gIobj be self.  
      */
     verifyPutInInterior()
     {
         /* 
          *   if we haven't resolved the direct object yet, we can at least
          *   check to see if all of the potential direct objects are
          *   already in me, and rule out this indirect object as illogical
          *   if so 
          */
         if (gDobj == nil)
         {
             /* 
              *   check the tentative direct objects to see if (1) all of
              *   them are directly inside me already, or (2) all of them
              *   are at least indirectly inside me already 
              */
             if (gTentativeDobj.indexWhich(
                 {x: !x.obj_.isDirectlyIn(self)}) == nil)
             {
                 /*
                  *   All of the potential direct objects are already
                  *   directly inside me.  This makes this object
                  *   illogical, since there's no need to move any of these
                  *   objects into me.  
                  */
                 illogicalAlready(&alreadyPutInMsg);
             }
             else if (gTentativeDobj.indexWhich(
                 {x: !x.obj_.isIn(self)}) == nil)
             {
                 /* 
                  *   All of the potential direct objects are already in
                  *   me, at least indirectly.  This makes this object
                  *   somewhat less likely, since we're more likely to want
                  *   to put something in here that wasn't already within.
                  *   Note that this isn't actually illogical, though,
                  *   since we could be moving something from deeper inside
                  *   me to directly inside me.  
                  */
                 logicalRank(50, 'dobjs already inside');
             }
         }
         else
         {
             /* 
              *   We can't put myself in myself, obviously.  We also can't
              *   put something into any component of itself, so the command
              *   is illogical if we're a component of the direct object. 
              */
             if (gDobj == self || isComponentOf(gDobj))
                 illogicalSelf(&cannotPutInSelfMsg);
 
             /* if it's already directly inside me, this is illogical */
             if (gDobj.isDirectlyIn(self))
                 illogicalAlready(&alreadyPutInMsg);
         }
 
         /* 
          *   if I'm not held by the actor, give myself a slightly lower
          *   ranking than fully logical, so that objects being held are
          *   preferred 
          */
         if (!isIn(gActor))
             logicalRank(60, 'not indirectly held');
         else if (!isHeldBy(gActor))
             logicalRank(70, 'not held');
     }
 
     /*
      *   Flag: reveal any hidden items contained directly within me when
      *   my interior is explicitly examined, via a command such as LOOK IN
      *   <self>.  By default, we reveal our hidden contents on
      *   examination; hidden objects are in most cases meant to be more
      *   inconspicuous than actually camouflaged, so a careful, explicit
      *   examination would normally reveal them.  If our hidden objects
      *   are so concealed that even explicit examination of our interior
      *   wouldn't reveal them, set this to nil.  
      */
     revealHiddenItems = true
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   A basic container is an object that can enclose its contents.  This is
  *   the core of the Container type, but this class only has the bare-bones
  *   sense-related enclosing features, without any action implementation.
  *   This can be used for cases where an object isn't meant to have its
  *   contents be manipulable by the player (so we don't want to allow "put
  *   in" and so on), but where we do want the ability to conceal our
  *   contents when we're closed.  
  */
 class BasicContainer: BulkLimiter
     /* 
      *   My current open/closed state.  By default, this state never
      *   changes, but is fixed in the object's definition; for example, a
      *   box without a lid would always be open, while a hollow glass cube
      *   would always be closed.  Our default state is open. 
      */
     isOpen = true
 
     /* the material that we're made of */
     material = adventium
 
     /* prepositional phrase for objects being put into me */
     putDestMessage = &putDestContainer
 
     /*
      *   Determine if I can move an object via a path through this
      *   container. 
      */
     checkMoveViaPath(obj, dest, op)
     {
         /* 
          *   if we're moving the object in or out of me, we must consider
          *   our openness and whether or not the object fits through our
          *   opening 
          */
         if (op is in (PathIn, PathOut))
         {
             /* if we're closed, we can't move anything in or out */
             if (!isOpen)
                 return new CheckStatusFailure(cannotMoveThroughMsg,
                                               obj, self);
 
             /* if it doesn't fit through our opening, don't allow it */
             if (!canFitObjThruOpening(obj))
                 return new CheckStatusFailure(op == PathIn
                                               ? &cannotFitIntoOpeningMsg
                                               : &cannotFitOutOfOpeningMsg,
                                               obj, self);
         }
         
         /* in any other cases, allow the operation */
         return checkStatusSuccess;
     }
 
     /* 
      *   The message property we use when we can't move an object through
      *   the containment boundary.  This is a playerActionMessages
      *   property.  
      */
     cannotMoveThroughMsg = &cannotMoveThroughContainerMsg
 
     /*
      *   Determine if an actor can touch an object via a path through this
      *   container.  
      */
     checkTouchViaPath(obj, dest, op)
     {
         /* 
          *   if we're reaching from inside directly to me, allow it -
          *   treat this as touching our interior, which we allow from
          *   within regardless of our open/closed status 
          */
         if (op == PathOut && dest == self)
             return checkStatusSuccess;
 
         /* 
          *   if we're reaching in or out of me, consider our openness and
          *   whether or not the actor's hand fits through our opening 
          */
         if (op is in (PathIn, PathOut))
         {
             /* if we're closed, we can't reach into/out of the container */
             if (!isOpen)
                 return new CheckStatusFailure(cannotTouchThroughMsg,
                                               obj, self);
 
             /* 
              *   if the object's "hand" doesn't fit through our opening,
              *   don't allow it 
              */
             if (!canObjReachThruOpening(obj))
                 return new CheckStatusFailure(op == PathIn
                                               ? &cannotReachIntoOpeningMsg
                                               : &cannotReachOutOfOpeningMsg,
                                               obj, self);
         }
         
         /* in any other cases, allow the operation */
         return checkStatusSuccess;
     }
 
     /* 
      *   Library message (in playerActionMessages) explaining why we can't
      *   touch an object through this container.  This is used when an
      *   actor on the outside tries to reach something on the inside, or
      *   vice versa.  
      */
     cannotTouchThroughMsg = &cannotTouchThroughContainerMsg
 
     /*
      *   Determine if the given object fits through our opening.  This is
      *   only called when we're open; this determines if the object can be
      *   moved in or out of this container.  By default, we'll return
      *   true; some objects might want to override this to disallow
      *   objects over a certain size from being moved in or out of this
      *   container.
      *   
      *   Note that this method doesn't care whether or not the object can
      *   actually fit inside the container once through the opening; we
      *   only care about whether or not the object can fit through the
      *   opening itself.  This allows for things like narrow-mouthed
      *   bottles which have greater capacity within than in their
      *   openings.  
      */
     canFitObjThruOpening(obj) { return true; }
 
     /*
      *   Determine if the given object can "reach" through our opening,
      *   for the purposes of touching an object on the other side of the
      *   opening.  This is used to determine if the object, which is
      *   usually an actor, can its "hand" (or whatever appendange 'obj'
      *   uses to reach things) through our opening.  This is only called
      *   when we're open.  By default, we'll simply return true.
      *   
      *   This differs from canFitObjThruOpening() in that we don't care if
      *   all of 'obj' is able to fit through the opening; we only care
      *   whether obj's hand (or whatever it uses for reaching) can fit.  
      */
     canObjReachThruOpening(obj) { return true; }
 
     /*
      *   Determine how a sense passes to my contents.  If I'm open, the
      *   sense passes through directly, since there's nothing in the way.
      *   If I'm closed, the sense must pass through my material.  
      */
     transSensingIn(sense)
     {
         if (isOpen)
         {
             /* I'm open, so the sense passes through without interference */
             return transparent;
         }
         else
         {
             /* I'm closed, so the sense must pass through my material */
             return material.senseThru(sense);
         }
     }
 
     /*
      *   Get my fill medium.  If I'm open, inherit my parent's medium,
      *   assuming that the medium behaves like fog or smoke and naturally
      *   disperses to fill any nested open containers.  If I'm closed, I
      *   am by default filled with no medium.  
      */
     fillMedium()
     {
         if (isOpen && location != nil)
         {
             /* I'm open, so return my location's medium */
             return location.fillMedium();
         }
         else
         {
             /* 
              *   I'm closed, so we're cut off from the parent - assume
              *   we're filled with nothing 
              */
             return nil;
         }
     }
 
     /*
      *   Display a message explaining why we are obstructing a sense path
      *   to the given object.
      */
     cannotReachObject(obj)
     {
         /* 
          *   We must be obstructing by containment.  Show an appropriate
          *   message depending on whether the object is inside me or not -
          *   if not, then the actor trying to reach the object must be
          *   inside me. 
          */
         if (obj.isIn(self))
             gLibMessages.cannotReachContents(obj, self);
         else
             gLibMessages.cannotReachOutside(obj, self);
     }
 
     /* explain why we can't see the source of a sound */
     cannotSeeSoundSource(obj)
     {
         /* we must be obstructing by containment */
         if (obj.isIn(self))
             gLibMessages.soundIsFromWithin(obj, self);
         else
             gLibMessages.soundIsFromWithout(obj, self);
     }
 
     /* explain why we can't see the source of an odor */
     cannotSeeSmellSource(obj)
     {
         /* we must be obstructing by containment */
         if (obj.isIn(self))
             gLibMessages.smellIsFromWithin(obj, self);
         else
             gLibMessages.smellIsFromWithout(obj, self);
     }
 
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Container: an object that can have other objects placed within it.  
  */
 class Container: BasicContainer
     /*
      *   Our fixed "look in" description, if any.  This is shown on LOOK
      *   IN before our normal listing of our portable contents; it can be
      *   used to describe generally what the interior looks like, for
      *   example.  By default, we show nothing here.  
      */
     lookInDesc = nil
 
     /* 
      *   Show our status for "examine".  This shows our open/closed status,
      *   and lists our contents. 
      */
     examineStatus()
     {
         /* show any special container-specific status */
         examineContainerStatus();
 
         /* inherit the default handling to show my contents */
         inherited();
     }
 
     /*
      *   mention my open/closed status for Examine processing 
      */
     examineContainerStatus()
     {
         /*
          *   By default, show nothing extra.  This can be overridden by
          *   subclasses as needed to show any extra status before our
          *   contents list.  
          */
     }
 
     /*
      *   Try putting an object into me when I'm serving as a bag of
      *   holding.  For a container, this simply does a "put obj in bag".  
      */
     tryPuttingObjInBag(target)
     {
         /* if the object won't fit all by itself, don't even try */
         if (target.getBulk() > maxSingleBulk)
             return nil;
 
         /* if we can't fit the object with other contents, don't try */
         if (target.whatIf({: getBulkWithin() > bulkCapacity},
                           &moveInto, self))
             return nil;
 
         /* we're a container, so use "put in" to get the object */
         return tryImplicitActionMsg(&announceMoveToBag, PutIn, target, self);
     }
 
     /* 
      *   Try moving an object into this container.  For a container, this
      *   performs a PUT IN command to move the object into self.  
      */
     tryMovingObjInto(obj) { return tryImplicitAction(PutIn, obj, self); }
 
     /* -------------------------------------------------------------------- */
     /*
      *   "Look in" 
      */
     dobjFor(LookIn)
     {
         verify() { }
         check()
         {
             /* 
              *   If I'm closed, and I can't see my contents when closed, we
              *   can't go on.  Unless, of course, the actor is inside us,
              *   in which case our external boundary isn't relevant.  
              */
             if (!isOpen
                 && transSensingIn(sight) == opaque
                 && !gActor.isIn(self))
             {
                 /* we can't see anything because we're closed */
                 reportFailure(&cannotLookInClosedMsg);
                 exit;
             }
         }
         action()
         {
             /* show our fixed "look in" description, if any */
             lookInDesc;
 
             /* examine my interior */
             examineInterior();
         }
     }
 
     /*
      *   "Search".  This is mostly like Open, except that the actor has to
      *   be able to reach into the object, not just see into it - searching
      *   implies a more thorough sort of examination, usually including
      *   physically poking through the object's contents.  
      */
     dobjFor(Search)
     {
         check()
         {
             /* 
              *   if I'm closed, and the actor isn't inside me, make sure my
              *   contents are reachable from the outside 
              */
             if (!isOpen
                 && transSensingIn(touch) != transparent
                 && !gActor.isIn(self))
             {
                 /* we can't search an object that we can't reach into */
                 reportFailure(&cannotTouchThroughMsg, gActor, self);
                 exit;
             }
         }
     }
     
 
     /* -------------------------------------------------------------------- */
     /*
      *   Put In processing.  A container can accept new contents. 
      */
 
     iobjFor(PutIn)
     {
         verify()
         {
             /* use the standard verification for adding new contents */
             verifyPutInInterior();
         }
 
         action()
         {
             /* move the direct object into me */
             gDobj.moveInto(self);
             
             /* issue our default acknowledgment of the command */
             defaultReport(&okayPutInMsg);
         }
     }
 ;
 
 
 /*
  *   A "restricted holder" is a generic mix-in class for various container
  *   types (Containers, Surfaces, Undersides, RearContainers, RearSurfaces)
  *   that adds a restriction to what can be contained.  
  */
 class RestrictedHolder: object
     /* 
      *   A list of acceptable items for the container.  This list can be
      *   used to identify the objects that can be put in the container (or
      *   on the surface, under the underside, or behind the rear container
      *   or surface).  
      */
     validContents = []
 
     /*
      *   Is the given object allowed to go in this container (or
      *   on/under/behind it, as appropriate for the type)?  Returns true if
      *   so, nil if not.  By default, we'll return true if the object is
      *   found in our validContents list, nil if not.  This can be
      *   overridden if a subclass wants to determine which objects are
      *   acceptable with some other kind of per-object test; for example, a
      *   subclass might accept only objects of a given class as contents,
      *   or might accept only contents with some particular attribute.  
      */
     canPutIn(obj) { return validContents.indexOf(obj) != nil; }
 
     /*
      *   Check a PUT IN/ON/UNDER/BEHIND action to ensure that the direct
      *   object is in our approved-contents list.  
      */
     checkPutDobj(msgProp)
     {
         /* validate the direct object */
         if (!canPutIn(gDobj))
         {
             /* explalin the problem */
             reportFailure(self.(msgProp)(gDobj));
 
             /* terminate the command */
             exit;
         }
     }
 ;
 
 
 /*
  *   A special kind of container that only accepts specific contents.  The
  *   acceptable contents can be specified by a list of enumerated items,
  *   or by a method that indicates whether or not an item is allowed.  
  */
 class RestrictedContainer: RestrictedHolder, Container
     /* 
      *   A message that explains why the direct object can't be put in this
      *   container.  In most cases, the rather generic default message
      *   should be overridden to provide a specific reason that the dobj
      *   can't be put in this object.  The rejected object is provided as a
      *   parameter in case the message needs to vary by object, but we
      *   ignore this and just use a single blanket failure message by
      *   default.  
      */
     cannotPutInMsg(obj) { return &cannotPutInRestrictedMsg; }
 
     /* override PutIn to enforce our contents restriction */
     iobjFor(PutIn) { check() { checkPutDobj(&cannotPutInMsg); } }
 ;
 
 /*
  *   A single container is a special kind of container that can only
  *   contain a single item.  If another object is put into this container,
  *   we'll remove any current contents.  
  */
 class SingleContainer: Container
     /* override PutIn to enforce our single-contents rule */
     iobjFor(PutIn)
     {
         preCond { return inherited() + objEmpty; }
     }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   OpenableContainer: an object that can contain things, and which can
  *   be opened and closed.  
  */
 class OpenableContainer: Openable, Container
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   LockableContainer: an object that can contain things, and that can be
  *   opened and closed as well as locked and unlocked.  
  */
 class LockableContainer: Lockable, OpenableContainer
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   KeyedContainer: an openable container that can be locked and
  *   unlocked, but only with a specified key.  
  */
 class KeyedContainer: LockableWithKey, OpenableContainer
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Surface: an object that can have other objects placed on top of it.
  *   A surface is essentially the same as a regular container, but the
  *   contents of a surface behave as though they are on the surface's top
  *   rather than contained within the object.  
  */
 class Surface: BulkLimiter
     /* 
      *   Our fixed LOOK IN description.  This is shown in response to LOOK
      *   IN before we list our portable contents; it can be used to show
      *   generally what the surface looks like.  By default, we say
      *   nothing here.  
      */
     lookInDesc = nil
 
     /* my contents lister */
     contentsLister = surfaceContentsLister
     descContentsLister = surfaceDescContentsLister
     lookInLister = surfaceLookInLister
     inlineContentsLister = surfaceInlineContentsLister
 
     /* 
      *   we're a surface, so taking something from me that's not among my
      *   contents shows the message as "that's not on the iobj" 
      */
     takeFromNotInMessage = &takeFromNotOnMsg
 
     /* 
      *   my message indicating that another object x cannot be put into me
      *   because I'm already in x 
      */
     circularlyInMessage = &circularlyOnMsg
 
     /* message phrase for objects put into me */
     putDestMessage = &putDestSurface
 
     /* message when we're too full for another object */
     tooFullMsg = &surfaceTooFullMsg
 
     /* 
      *   Try moving an object into this container.  For a surface, this
      *   performs a PUT ON command to move the object onto self.  
      */
     tryMovingObjInto(obj) { return tryImplicitAction(PutOn, obj, self); }
 
     /* -------------------------------------------------------------------- */
     /*
      *   Put On processing 
      */
     iobjFor(PutOn)
     {
         verify()
         {
             /* use the standard put-in verification */
             verifyPutInInterior();
         }
 
         action()
         {
             /* move the direct object onto me */
             gDobj.moveInto(self);
             
             /* issue our default acknowledgment */
             defaultReport(&okayPutOnMsg);
         }
     }
 
     /*
      *   Looking "in" a surface simply shows the surface's contents. 
      */
     dobjFor(LookIn)
     {
         verify() { }
         action()
         {
             /* show our fixed lookInDesc */
             lookInDesc;
 
             /* show our contents */
             examineInterior();
         }
     }
 
     /* use the PUT ON forms of the verifier messages */
     cannotPutInSelfMsg = &cannotPutOnSelfMsg
     alreadyPutInMsg = &alreadyPutOnMsg
 ;
 
 /*
  *   A special kind of surface that only accepts specific contents.
  */
 class RestrictedSurface: RestrictedHolder, Surface
     /* 
      *   A message that explains why the direct object can't be put on this
      *   surface.  In most cases, the rather generic default message should
      *   be overridden to provide a specific reason that the dobj can't be
      *   put on this surface.  The rejected object is provided as a
      *   parameter in case the message needs to vary by object, but we
      *   ignore this and just use a single blanket failure message by
      *   default.  
      */
     cannotPutOnMsg(obj) { return &cannotPutOnRestrictedMsg; }
 
     /* override PutOn to enforce our contents restriction */
     iobjFor(PutOn) { check() { checkPutDobj(&cannotPutOnMsg); } }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Food - something you can eat.  By default, when an actor eats a food
  *   item, the item disappears.  
  */
 class Food: Thing
     dobjFor(Taste)
     {
         /* tasting food is perfectly logical */
         verify() { }
     }
 
     dobjFor(Eat)
     {
         verify() { }
         action()
         {
             /* describe the consumption */
             defaultReport(&okayEatMsg);
 
             /* the object disappears */
             moveInto(nil);
         }
     }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   OnOffControl - a generic control that can be turned on and off.  We
  *   keep track of an internal on/off state, and recognize the commands
  *   "turn on" and "turn off".  
  */
 class OnOffControl: Thing
     /*
      *   The current on/off setting.  We'll start in the 'off' position by
      *   default.  
      */
     isOn = nil
 
     /*
      *   On/off status name.  This returns the appropriate name ('on' or
      *   'off' in English) for our current status. 
      */
     onDesc = (isOn ? gLibMessages.onMsg(self) : gLibMessages.offMsg(self))
 
     /*
      *   Change our on/off setting.  Subclasses can override this to apply
      *   any side effects of changing the value. 
      */
     makeOn(val)
     {
         /* remember the new value */
         isOn = val;
     }
 
     dobjFor(TurnOn)
     {
         verify()
         {
             /* if it's already on, complain */
             if (isOn)
                 illogicalAlready(&alreadySwitchedOnMsg);
         }
         action()
         {
             /* set to 'on' and generate a default report */
             makeOn(true);
             defaultReport(&okayTurnOnMsg);
         }
     }
 
     dobjFor(TurnOff)
     {
         verify()
         {
             /* if it's already off, complain */
             if (!isOn)
                 illogicalAlready(&alreadySwitchedOffMsg);
         }
         action()
         {
             /* set to 'off' and generate a default report */
             makeOn(nil);
             defaultReport(&okayTurnOffMsg);
         }
     }
 ;
 
 /*
  *   Switch - a simple extension of the generic on/off control that can be
  *   used with a "switch" command without specifying "on" or "off", and
  *   treats "flip" synonymously.  
  */
 class Switch: OnOffControl
     /* "switch" with no specific new setting - reverse our setting */
     dobjFor(Switch)
     {
         verify() { }
         action()
         {
             /* reverse our setting and generate a report */
             makeOn(!isOn);
             defaultReport(isOn ? &okayTurnOnMsg : &okayTurnOffMsg);
         }
     }
 
     /* "flip" is the same as "switch" for our purposes */
     dobjFor(Flip) asDobjFor(Switch)
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Settable - an abstract class for things you can set to different
  *   settings; the settings can be essentially anything, such as numbers
  *   (or other markers) on a dial, or stops on a sliding switch. 
  */
 class Settable: Thing
     /*
      *   Our current setting.  This is an arbitrary string value.  The
      *   value initially assigned here is our initial setting; we'll
      *   update this whenever we're set to another setting.  
      */
     curSetting = '1'
 
     /*
      *   Canonicalize a proposed setting.  This ensures that the setting is
      *   in a specific primary format when there are superficially
      *   different ways of expressing the same value.  For example, if the
      *   setting is numeric, this could do things like trim off leading
      *   zeroes; for a text value, it could ensure the value is in the
      *   proper case.  
      */
     canonicalizeSetting(val)
     {
         /* 
          *   by default, we don't have any special canonical format, so
          *   just return the value as it is 
          */
         return val;
     }
 
     /*
      *   Change our setting.  This is always called with the canonical
      *   version of the new setting, as returned by canonicalizeSetting().
      *   Subclasses can override this routine to apply any side effects of
      *   changing the value.  
      */
     makeSetting(val)
     {
         /* remember the new value */
         curSetting = val;
     }
 
     /*
      *   Is the given text a valid setting?  Returns true if so, nil if
      *   not.  This should not display any messages; simply indicate
      *   whether or not the setting is valid.
      *   
      *   This is always called with the *canonical* value of the proposed
      *   new setting, as returned by canonicalizeSetting().  
      */
     isValidSetting(val)
     {
         /* 
          *   By default, allow anything; subclasses should override to
          *   enforce our valid set of values.  
          */
         return true;
     }
 
     /*
      *   "set <self>" action 
      */
     dobjFor(Set)
     {
         verify() { logicalRank(150, 'settable'); }
         action() { askForLiteral(SetTo); }
     }
 
     /*
      *   "set <self> to <literal>" action
      */
     dobjFor(SetTo)
     {
         preCond = [touchObj]
         verify()
         {
             local txt;
             
             /* 
              *   If we already know our literal text, and it's not valid,
              *   reduce the logicalness.  Don't actually make it
              *   illogical, as it's probably still more logical to set a
              *   settable to an invalid setting than to set something that
              *   isn't settable at all.  
              */
             if ((txt = gAction.getLiteral()) != nil
                 && !isValidSetting(canonicalizeSetting(txt)))
                 logicalRank(50, 'invalid setting');
         }
         check()
         {
             /* if the setting is not valid, don't allow it */
             if (!isValidSetting(canonicalizeSetting(gAction.getLiteral())))
             {
                 /* there is no such setting */
                 reportFailure(setToInvalidMsg);
                 exit;
             }
         }
         action()
         {
             /* set the new value */
             makeSetting(canonicalizeSetting(gAction.getLiteral()));
 
             /* remark on the change */
             defaultReport(okaySetToMsg, curSetting);
         }
     }
 
     /* our message property for an invalid setting */
     setToInvalidMsg = &setToInvalidMsg
 
     /* our message property for acknowledging a new setting */
     okaySetToMsg = &okaySetToMsg
 ;
 
 /*
  *   Dial - something you can turn to different settings.  Note that dials
  *   are usually used as components of larger objects; since our base
  *   class is the basic Settable, component dials should be created to
  *   inherit multiply from Dial and Component, in that order.
  *   
  *   This is almost hte same as a regular Settable; the only thing we add
  *   is that we make "turn <self> to <literal>" equivalent to "set <self>
  *   to <literal>", as this is the verb most people would use to set a
  *   dial.  
  */
 class Dial: Settable
     /* "turn" with no destination - indicate that we need a setting */
     dobjFor(Turn)
     {
         verify() { illogical(&mustSpecifyTurnToMsg); }
     }
 
     /* treat "turn <self> to <literal>" the same as "set to" */
     dobjFor(TurnTo) asDobjFor(SetTo)
 
     /* refer to setting the dial as turning it in our messages */
     setToInvalidMsg = &turnToInvalidMsg
     okaySetToMsg = &okayTurnToMsg
 ;
 
 /*
  *   Numbered Dial - something you can turn to a range of numeric values. 
  */
 class NumberedDial: Dial
     /*
      *   The range of settings - the dial can be set to values from the
      *   minimum to the maximum, inclusive. 
      */
     minSetting = 1
     maxSetting = 10
 
     /*
      *   Canonicalize a proposed setting value.  For numbers, strip off any
      *   leading zeroes, since these don't change the meaning of the value.
      */
     canonicalizeSetting(val)
     {
         local num;
         
         /* try parsing it as a digit string or a spelled-out number */
         if ((num = parseInt(val)) != nil)
         {
             /* 
              *   we parsed it successfully - return the string
              *   representation of the numeric value 
              */
             return toString(num);
         }
 
         /* it didn't parse as a number, so just return it as-is */
         return val;
     }
 
     /* 
      *   Check a setting for validity.  A setting is valid only if it's a
      *   number within the allowed range for the dial. 
      */
     isValidSetting(val)
     {
         local num;
         
         /* if it doesn't look like a number, it's not valid */
         if (rexMatch('<digit>+', val) != val.length())
             return nil;
 
         /* get the numeric value */
         num = toInteger(val);
 
         /* it's valid if it's within range */
         return num >= minSetting && num <= maxSetting;
     }
 ;
 
 /*
  *   Labeled Dial - something you can turn to a set of arbitrary text
  *   labels. 
  */
 class LabeledDial: Dial
     /*
      *   The list of valid settings.  Each entry in this list should be a
      *   string value.  We ignore the case of these labels (we convert
      *   everything to upper-case when comparing labels).  
      */
     validSettings = []
 
     /*
      *   Canonicalize the setting.  We consider case insignificant in
      *   matching our labels, but the canonical version of a setting is the
      *   one that appears in the validSettings list - so if the player
      *   types in SET DIAL TO EXTRA LOUD, and the validSettings list
      *   contains 'Extra Loud', we'll want to convert the 'EXTRA LOUD' to
      *   the capitalization of the validSettings entry.  
      */
     canonicalizeSetting(val)
     {
         local txt;
         
         /* 
          *   convert it to upper-case, so that we can compare it to our
          *   valid labels without regard to case
          */
         txt = val.toUpper();
 
         /* 
          *   if we find a match in the validSettings list, return the match
          *   from the list, since that's the canonical format 
          */
         if ((txt = validSettings.valWhich({x: x.toUpper() == txt})) != nil)
             return txt;
 
         /* we didn't find a match, so leave the original value unchanged */
         return val;
     }
 
     /* 
      *   Check a setting for validity.  A setting is valid only if it
      *   appears in the validSettings list for this dial.
      */
     isValidSetting(val)
     {
         /* 
          *   If the given value appears in our validSettings list, it's a
          *   valid setting; otherwise, it's not valid.  Ignore case when
          *   comparing values by converting the valid labels to upper case;
          *   we've already converted the value we're testing to upper case,
          *   so the case mix won't matter in our comparison.
          *   
          *   Note that we're handed a canonical setting value, so we don't
          *   have to worry about case differences.  
          */
         return validSettings.indexOf(val) != nil;
     }
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Button - something you can push to activate, as a control for a
  *   mechanical device.  
  */
 class Button: Thing
     dobjFor(Push)
     {
         verify() { }
         action()
         {
             /* 
              *   individual buttons should override this to carry out any
              *   special action for the button; by default, we'll just
              *   show a simple acknowledgment  
              */
             defaultReport(&okayPushButtonMsg);
         }
     }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Lever - something you can push, pull, or move, generally as a control
  *   for a mechanical device.  Our basic lever has two states, "pushed"
  *   and "pulled".  
  */
 class Lever: Thing
     /*
      *   The current state.  We have two states: "pushed" and "pulled".
      *   We start in the pushed state, so the lever can initially be
      *   pulled, since "pull" is the verb most people would first think to
      *   apply to a lever.  
      */
     isPulled = nil
 
     /* 
      *   Set the state.  This can be overridden to apply side effects as
      *   needed. 
      */
     makePulled(pulled)
     {
         /* note the new state */
         isPulled = pulled;
     }
 
     /*
      *   Action handlers.  We handle push and pull, and we treat "move" as
      *   equivalent to whichever of push or pull is appropriate to reverse
      *   the current state.  
      */
     dobjFor(Push)
     {
         verify()
         {
             /* if it's already pushed, pushing it again makes no sense */
             if (!isPulled)
                 illogicalAlready(&alreadyPushedMsg);
         }
         action()
         {
             /* set the new state to pushed (i.e., not pulled) */
             makePulled(nil);
 
             /* make the default report */
             defaultReport(&okayPushLeverMsg);
         }
     }
     dobjFor(Pull)
     {
         verify()
         {
             /* if it's already pulled, pulling it again makes no sense */
             if (isPulled)
                 illogicalAlready(&alreadyPulledMsg);
         }
         action()
         {
             /* set the new state to pulled */
             makePulled(true);
 
             /* make the default report */
             defaultReport(&okayPullLeverMsg);
         }
     }
     dobjFor(Move)
     {
         verify() { }
         check()
         {
             /* run the check for pushing or pulling, as appropriate */
             if (isPulled)
                 checkDobjPush();
             else
                 checkDobjPull();
         }
         action()
         {
             /* if we're pulled, push the lever; otherwise pull it */
             if (isPulled)
                 actionDobjPush();
             else
                 actionDobjPull();
         }
     }
 ;
 
 /*
  *   A spring-loaded lever is a lever that bounces back to its starting
  *   position after being pulled.  This is essentially equivalent in terms
  *   of functionality to a button, but can at least provide superficial
  *   variety.  
  */
 class SpringLever: Lever
     dobjFor(Pull)
     {
         action()
         {
             /*
              *   Individual objects should override this to perform the
              *   appropriate action when the lever is pulled.  By default,
              *   we'll do nothing except show a default report. 
              */
             defaultReport(&okayPullSpringLeverMsg);
         }
     }
 ;
     
 
 /* ------------------------------------------------------------------------ */
 /*
  *   An item that can be worn
  */
 class Wearable: Thing
     /* is the item currently being worn? */
     isWorn()
     {
         /* it's being worn if the wearer is non-nil */
         return wornBy != nil;
     }
 
     /* 
      *   make the item worn by the given actor; if actor is nil, the item
      *   isn't being worn by anyone 
      */
     makeWornBy(actor)
     {
         /* remember who's wearing the item */
         wornBy = actor;
     }
 
     /*
      *   An item being worn is not considered to be held in the wearer's
      *   hands. 
      */
     isHeldBy(actor)
     {
         if (isWornBy(actor))
         {
             /* it's being worn by the actor, so it's not also being held */
             return nil;
         }
         else
         {
             /* 
              *   it's not being worn by this actor, so use the default
              *   interpretation of being held 
              */
             return inherited(actor);
         }
     }
 
     /*
      *   A wearable is not considered held by an actor when it is being
      *   worn, so we must do a what-if test for removing the item if the
      *   actor is currently wearing the item.  If the actor isn't wearing
      *   the item, we can use the default test of moving the item into the
      *   actor's inventory.  
      */
     whatIfHeldBy(func, newLoc)
     {
         /*
          *   If the article is being worn, and it's already in the same
          *   location we're moving it to, simply test with the article no
          *   longer being worn.  Otherwise, inherit the default handling.  
          */
         if (location == newLoc && wornBy != nil)
             return whatIf(func, &wornBy, nil);
         else
             return inherited(func, newLoc);
     }
 
     /*
      *   Try making the current command's actor hold me.  If I'm already
      *   directly in the actor's inventory and I'm being worn, we'll try a
      *   'doff' command; otherwise, we'll use the default handling.  
      */
     tryHolding()
     {
         /*   
          *   Try an implicit 'take' command.  If the actor is carrying the
          *   object indirectly, make the command "take from" instead,
          *   since what we really want to do is take the object out of its
          *   container.  
          */
         if (location == gActor && isWornBy(gActor))
             return tryImplicitAction(Doff, self);
         else
             return inherited();
     }
 
     /* 
      *   The object wearing this object, if any; if I'm not being worn,
      *   this is nil.  The wearer should always be a container (direct or
      *   indirect) of this object - in order to wear something, you must
      *   be carrying it.  In most cases, the wearer should be the direct
      *   container of the object.
      *   
      *   The reason we keep track of who's wearing the object (rather than
      *   simply keeping track of whether it's being worn) is to allow for
      *   cases where an actor is carrying another actor.  Since this
      *   object will be (indirectly) inside both actors in such cases, we
      *   would have to inspect intermediate containers to determine
      *   whether or not the outer actor was wearing the object if we
      *   didn't keep track of the wearer directly.  
      */
     wornBy = nil
 
     /* am I worn by the given object? */
     isWornBy(actor)
     {
         return wornBy == actor;
     }
 
     /*
      *   An article of clothing that is being worn by an actor does not
      *   typically encumber the actor at all, so by default we'll return
      *   zero if we're being worn by the actor, and our normal bulk
      *   otherwise.  
      */
     getEncumberingBulk(actor)
     {
         /* 
          *   if we're being worn by the actor, we create no encumbrance at
          *   all; otherwise, return our normal bulk 
          */
         return isWornBy(actor) ? 0 : getBulk();
     }
 
     /*
      *   An article of clothing typically encumbers an actor with the same
      *   weight whether or not the actor is wearing the item.  However,
      *   this might not apply to all objects; a suit of armor, for
      *   example, might be slightly less encumbering in terms of weight
      *   when worn than it is when held because the distribution of weight
      *   is more manageable when worn.  By default, we simply return our
      *   normal weight, whether worn or not; subclasses can override as
      *   needed to differentiate.  
      */
     getEncumberingWeight(actor)
     {
         return getWeight();
     }
 
     /* get my state */
     getState = (isWorn() ? wornState : unwornState)
 
     /* my list of possible states */
     allStates = [wornState, unwornState]
 
 
     /* -------------------------------------------------------------------- */
     /*
      *   Action processing 
      */
 
     dobjFor(Wear)
     {
         preCond = [objHeld]
         verify()
         {
             /* make sure the actor isn't already wearing the item */
             if (isWornBy(gActor))
                 illogicalAlready(&alreadyWearingMsg);
         }
         action()
         {
             /* make the item worn and describe what happened */
             makeWornBy(gActor);
             defaultReport(&okayWearMsg);
         }
     }
 
     dobjFor(Doff)
     {
         preCond = [roomToHoldObj]
         verify()
         {
             /* 
              *   Make sure the actor is actually wearing the item.  If
              *   they're not, it's illogical, but if they are, it's an
              *   especially likely thing to remove. 
              */
             if (!isWornBy(gActor))
                 illogicalAlready(&notWearingMsg);
             else
                 logicalRank(150, 'worn');
         }
         action()
         {
             /* un-wear the item and describe what happened */
             makeWornBy(nil);
             defaultReport(&okayDoffMsg);
         }
     }
 
     /* "remove <wearable>" is the same as "doff <wearable>" */
     dobjFor(Remove) asDobjFor(Doff)
 
     /* 
      *   if a wearable is being worn, showing it off to someone doesn't
      *   require holding it 
      */
     dobjFor(ShowTo)
     {
         preCond()
         {
             /* get the standard handling */
             local lst = inherited();
 
             /* if we're being worn, don't require us to be held */
             if (isWornBy(gActor))
                 lst -= objHeld;
 
             /* return the result */
             return lst;
         }
     }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   An item that can provide light.
  *   
  *   Any Thing can provide light, but this class should be used for
  *   objects that explicitly serve as light sources from the player's
  *   perspective.  Objects of this class display a "providing light"
  *   status message in inventory listings, and can be turned on and off
  *   via the isLit property.  
  */
 class LightSource: Thing
     /* is the light source currently turned on? */
     isLit = true
 
     /*
      *   Turn the light source on or off.  Note that we don't have to make
      *   any special check for a change to the light level, because the
      *   main action handler always checks for a change in light/dark
      *   status over the course of the turn.  
      */
     makeLit(lit)
     {
         /* change the status */
         isLit = lit;
     }
 
     /* 
      *   We can distinguish light sources according to their isLit status.
      *   Give the lit/unlit distinction higher priority than the normal
      *   ownership/containment distinction. 
      */
     distinguishers = [basicDistinguisher, litUnlitDistinguisher,
                       ownershipDistinguisher, locationDistinguisher]
 
     /* the brightness that the object has when it is on and off */
     brightnessOn = 3
     brightnessOff = 0
 
     /* 
      *   return the appropriate on/off brightness, depending on whether or
      *   not we're currently lit 
      */
     brightness { return isLit ? brightnessOn : brightnessOff; }
 
     /* get our current state: lit or unlit */
     getState = (brightness > 1 ? lightSourceStateOn : lightSourceStateOff)
 
     /* get our set of possible states */
     allStates = [lightSourceStateOn, lightSourceStateOff]
 ;
 
 /*
  *   A Flashlight is a special kind of light source that can be switched
  *   on and off.
  *   
  *   To create a limited-use flashlight (with a limited battery life, for
  *   example), you can combine this class with FueledLightSource.  The
  *   flashlight's on/off switch status is a separate property from its
  *   lit/unlit light-source status, so combining Flashlight with
  *   FueledLightSource will actually allow the two to become decoupled: a
  *   flashlight can be on without providing light, when the battery is
  *   dead.  For this reason, you might want to override the decription,
  *   and possibly the TurnOn action() handler, to customize the messages
  *   for the case when the flashlight is switched on but out of power.  
  */
 class Flashlight: LightSource, Switch
     /* our switch status - start in the 'off' position */
     isOn = nil
 
     /* 
      *   Change the on/off status.  Note that switching the flashlight on
      *   or off should always be done via makeOn - the makeLit inherited
      *   from the LightSource should never be called directly on a
      *   Flashlight object, because it doesn't keep the switch on/off and
      *   flashlight lit/unlit status in sync.  This routine is the one to
      *   call because it keeps everything properly synchronized.  
      */
     makeOn(stat)
     {
         /* inherit the default handling */
         inherited(stat);
 
         /* 
          *   Set the 'lit' status to track the on/off status.  Note that
          *   we don't simply do this by deriving isLit from isOn because
          *   we want to invoke the side effects of changing the status by
          *   calling makeLit explicitly.  We also want to allow the two to
          *   be decoupled when necessary, such as might happen when the
          *   flashlight's bulb is burned out, or its battery has run down.
          */
         makeLit(stat);
     }
 
     /* initialize */
     initializeThing()
     {
         /* inherit default handling */
         inherited();
 
         /* 
          *   Make sure our initial isLit setting (for the LightSource)
          *   matches our initial isOn steting (for the Switch).  The
          *   switch status drives the light source status, so initialize
          *   the latter from the former.  
          */
         isLit = isOn;
     }
 
     /* treat 'light' and 'extinguish' as 'turn on' and 'turn off' */
     dobjFor(Light) asDobjFor(TurnOn)
     dobjFor(Extinguish) asDobjFor(TurnOff)
 
     /* if we turn on the flashlight, but it doesn't light, mention this */
     dobjFor(TurnOn)
     {
         action()
         {
             /* do the normal work */
             inherited();
 
             /* 
              *   If we're now on but not lit, mention this.  This can
              *   happen when we run out of power in the battery, or our
              *   bulb is missing or burned out, or we're simply broken. 
              */
             if (isOn && !isLit)
                 mainReport(&flashlightOnButDarkMsg);
         }
     }
 ;
 
TADS 3 Library Manual
Generated on 9/15/2006 from TADS version 3.0.12