# 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.
     */
    class 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 nil
         *   when the object leaves sense scope, so the sequence of messages
         *   will automatically start over each time the object comes back into
         *   scope.
         *
         *   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;

            /* reset the display count */
            displayCount = 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(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(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(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(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.
     *   Note that if you use this approach, the CollectiveGroup should *also*
     *   inherit from Fixture or the like, so that the group object is fixed in
     *   place just like its corresponding individuals.
     *
     *   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, 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(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(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(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)
                {
                    /* get the room contents lister */
                    local lister = gActor.location.roomContentsLister;

                    /* get the subset that the room contents lister won't list */
                    local xlist = here.subset({x: !lister.isListed(x)});

                    /* show the list through the room contents lister */
                    lister.showList(gActor, nil, here, 0, 0, info, nil);

                    /* Examine any objects not part of the room description */
                    foreach (local x in xlist)
                        examineUnlisted(x);

                    /*
                     *   if that showed anything, add a paragraph break before
                     *   the carried list
                     */
                    if (xlist.length() != 0 && carried.length() != 0)
                        "<.p>";
                }

                /* 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);
            }
        }

        /*
         *   Examine an unlisted individual object.  This will be called for
         *   each object in the room that's not listable via the room contents
         *   lister.
         */
        examineUnlisted(x)
        {
            "<.p>";
            nestedAction(Examine, x);
        }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   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 check() 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) { 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 and SEARCH */
        dobjFor(LookIn)
            { verify() { inherited(); logicalRank(70, 'decoration'); } }
        dobjFor(Search)
            { 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()
        {
            /*
             *   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 [];

            /* start with an empty list */
            local 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);
        }

        /*
         *   Determine if I'm to be listed within my immediate container.  As a
         *   multi-location object, we have multiple immediate containers, so
         *   we need to know which direct container we're talking about.
         *   Thing.examineListContents() passes this down via "cont:", a named
         *   parameter.  Other callers might not always provide this argument,
         *   though, so if it's not present simply base this on whether we have
         *   a special description in any context.
         */
        isListedInContents(examinee:?)
        {
            return (examinee != nil
                    ? !useSpecialDescInContents(examinee)
                    : !useSpecialDesc());
        }

        /* Am I either inside 'obj', or equal to 'obj'?  */
        isOrIsIn(obj) { return self == obj || isIn(obj); }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   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 CollectiveGroup objects */
            foreach (local cur in collectiveGroups)
                tab[cur] = 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 our
             *   own point of view, or from a point of view inside us, we do
             *   get to see out to all of our containers.
             */
            if (senseTmp.pointOfView == self || 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)
        {
            /*
             *   If I'm not already open, try opening me.  As usual for 'try'
             *   routines, we return true if we attempt a command, nil if not.
             *
             *   Note that we might be creating an obstruction despite already
             *   being open; in this case, we don't want to do anything, since
             *   an implied 'open' won't help when we're already open.
             */
            return isOpen ? nil : 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 = openableDescContentsLister

        /*
         *   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;

                /*
                 *   searching implies physically sifting through the contents,
                 *   so we need to be able to touch the object
                 */
                lst += touchObj;

                /* 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(stat);
        }

        /* 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's current locked state.  In English,
         *   this simply returns one of 'locked' or 'unlocked'.  (Note that
         *   this is provided as a convenience to games, for generating
         *   messages about the object that include its state.  The library
         *   doesn't use this message itself, so overriding this won't change
         *   any library messages - in particular, it won't change the
         *   examineStatus message.)
         */
        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;
                }

                /* inherit the default handling */
                inherited();
            }
        }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   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 that 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);
        }

        /* message when an object is too large (all by itself) to fit in me */
        tooLargeForContainerMsg = &tooLargeForContainerMsg
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   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)
        {
            preCond = (nilToList(inherited()) + [touchObj])
            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))
            {
                /* explain 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
         *   zeros; 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(setToInvalidMsgProp);
                    exit;
                }
            }
            action()
            {
                /* set the new value */
                makeSetting(canonicalizeSetting(gAction.getLiteral()));

                /* remark on the change */
                defaultReport(okaySetToMsgProp, curSetting);
            }
        }

        /* our message property for an invalid setting */
        setToInvalidMsgProp = &setToInvalidMsg

        /* our message property for acknowledging a new setting */
        okaySetToMsgProp = &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 */
        setToInvalidMsgProp = &turnToInvalidMsg
        okaySetToMsgProp = &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 zeros, 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 5/16/2013 from TADS version 3.1.3