# travel.t | documentation

    #charset "us-ascii"

    /*
     *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved.
     *
     *   TADS 3 Library - travel
     *
     *   This module defines the parts of the simulation model related to
     *   travel: rooms and other locations, directions, passages.
     */

    /* include the library header */
    #include "adv3.h"

    infiniteLoop()
    {
        for (local i = 1 ; i < 100 ; ) ;
    }

    /* ------------------------------------------------------------------------ */
    /*
     *   Directions.  Each direction is represented by an instance of
     *   Direction.
     *
     *   A Direction object encapsulates information about a travel direction.
     *   By using an object to represent each possible travel direction, we
     *   make it possible for a game or library extension to add new custom
     *   directions without having to change the basic library.
     */
    class Direction: object
        /*
         *   The link property for the direction.  This is a property pointer
         *   that we use to ask an actor's location for a TravelConnector when
         *   the actor attempts to travel in this direction.
         */
        dirProp = nil

        /*
         *   The default TravelConnector when an actor attempts travel in this
         *   direction, but the actor's location does not define the dirProp
         *   property for this direction.  This is almost always a connector
         *   that goes nowhere, such as noTravel, since this is used only when
         *   a location doesn't have a link for travel in this direction.
         */
        defaultConnector(loc) { return noTravel; }

        /*
         *   Initialize.  We'll use this routine to add each Direction
         *   instance to the master direction list (Direction.allDirections)
         *   during pre-initialization.
         */
        initializeDirection()
        {
            /* add myself to the master direction list */
            Direction.allDirections.append(self);
        }

        /*
         *   Class initialization - this is called once on the class object.
         *   We'll build our master list of all of the Direction objects in
         *   the game, and then sort the list using the sorting order.
         */
        initializeDirectionClass()
        {
            /* initialize each individual Direction object */
            forEachInstance(Direction, { dir: dir.initializeDirection() });

            /*
             *   sort the direction list according to the individual Directin
             *   objects' defined sorting orders
             */
            allDirections.sort(SortAsc, {a, b: a.sortingOrder - b.sortingOrder});
        }

        /*
         *   Our sorting order in the master list.  We use this to present
         *   directions in a consistent, aesthetically pleasing order in
         *   listings involving multiple directions.  The sorting order is
         *   simply an integer that gives the relative position in the list;
         *   the list of directions is sorted from lowest sorting order to
         *   highest.  Sorting order numbers don't have to be contiguous,
         *   since we simply put the directions in an order that makes the
         *   sortingOrder values ascend through the list.
         */
        sortingOrder = 1

        /*
         *   A master list of the defined directions.  We build this
         *   automatically during initialization to include each Direction
         *   instance.  Any operation that wants to iterate over all possible
         *   directions can run through this list.
         *
         *   By using this master list to enumerate all possible directions
         *   rather than a hard-coded set of pre-defined directions, we can
         *   ensure that any new directions that a game or library extension
         *   adds will be incorporated automatically into the library simply
         *   by virtue of the existence Direction instances to represent the
         *   new directions.
         */
        allDirections = static new Vector(10)
    ;

    /*
     *   The base class for compass directions (north, south, etc).  We don't
     *   add anything to the basic Direction class, but we use a separate
     *   class for compass directions to allow language-specific
     *   customizations for all of the directions and to allow travel commands
     *   to treat these specially when needed.
     */
    class CompassDirection: Direction
    ;

    /*
     *   The base class for shipboard directions (port, aft, etc).
     */
    class ShipboardDirection: Direction
        defaultConnector(loc)
        {
            /*
             *   If 'loc' is aboard a ship, use noTravel as the default
             *   connector, since we simply can't go this direction.  If we're
             *   not aboard ship, use the special connector noShipTravel,
             *   which is meant to convey that shipboard directions make no
             *   sense when not aboard a ship.
             */
            return (loc.isShipboard ? noTravel : noShipTravel);
        }
    ;

    /*
     *   The base class for vertical directions (up, down)
     */
    class VerticalDirection: Direction
    ;

    /*
     *   The base class for "relative" directions (in, out)
     */
    class RelativeDirection: Direction
    ;

    /*
     *   Individual compass directions.
     *
     *   Our macro defines a direction object with a name based on the
     *   direction's room travel link property and the base class.  So,
     *   DefineDirection(north, Compass) defines a direction object called
     *   'northDirection' based on the CompassDirection class, with the link
     *   property 'north' and default travel connector 'noTravel'.
     *
     *   Note that we define a sorting order among the default directions as
     *   follows.  First, we define several groups of related directions,
     *   which we put in a relative order: the compass directions, then the
     *   vertical directions, then the "relative" (in/out) directions, and
     *   finally the shipboard directions.  Then, we order the directions
     *   within these groups.  For the sortingOrder values, we use arbitrary
     *   integers with fairly wide separations, to leave plenty of room for
     *   custom game-specific directions to be added before, between, after,
     *   or within these pre-defined groups.
     */
    #define DefineDirection(prop, base, order) \
        prop##Direction: base##Direction \
        dirProp = &prop \
        sortingOrder = order

    DefineDirection(north, Compass, 1000);
    DefineDirection(south, Compass, 1100);
    DefineDirection(east, Compass,  1200);
    DefineDirection(west, Compass,  1300);
    DefineDirection(northeast, Compass, 1400);
    DefineDirection(northwest, Compass, 1500);
    DefineDirection(southeast, Compass, 1600);
    DefineDirection(southwest, Compass, 1700);

    DefineDirection(up, Vertical, 3000);
    DefineDirection(down, Vertical, 3100);

    DefineDirection(in, Relative, 5000)
        defaultConnector(loc) { return noTravelIn; }
    ;

    DefineDirection(out, Relative, 5100)
        defaultConnector(loc) { return noTravelOut; }
    ;

    DefineDirection(fore, Shipboard, 7000);
    DefineDirection(aft, Shipboard, 7100);
    DefineDirection(port, Shipboard, 7200);
    DefineDirection(starboard, Shipboard, 7300);

    /* ------------------------------------------------------------------------ */
    /*
     *   Travel Message Handler.  This contains a set of messages that are
     *   specific to different types of TravelConnector objects, to describe
     *   NPC arrivals and departures via these connectors when the NPC's are in
     *   view of the player character.
     *
     *   This base class implements the methods simply by calling the
     *   corresponding gLibMessages message methods.
     *
     *   The purpose of providing this variety of connector-specific handlers
     *   is to make it easy for individual travelers to customize the
     *   arrival/departure messages for specific connector subclasses.  These
     *   messages sometimes benefit from customization for specific
     *   traveler/connector combinations; the easiest way to enable such
     *   granular customization is to make it possible to override a message
     *   per connector type on the traveler object.
     */
    class TravelMessageHandler: object
        /*
         *   Get the traveler for the purposes of arrival/departure messages.
         *   Implementations that aren't themselves the travelers should
         *   override this to supply the correct nominal traveler.
         */
        getNominalTraveler() { return self; }

        /* generic arrival/departure - for the base TravelConnector class */
        sayArriving(conn) { gLibMessages.sayArriving(getNominalTraveler()); }
        sayDeparting(conn) { gLibMessages.sayDeparting(getNominalTraveler()); }

        /* generic local arrival and departure messages */
        sayArrivingLocally(dest, conn)
            { gLibMessages.sayArrivingLocally(getNominalTraveler(), dest); }
        sayDepartingLocally(dest, conn)
            { gLibMessages.sayDepartingLocally(getNominalTraveler(), dest); }

        /* generic remote travel message */
        sayTravelingRemotely(dest, conn)
            { gLibMessages.sayTravelingRemotely(getNominalTraveler(), dest); }

        /* directional arrival/departure - for RoomConnector */
        sayArrivingDir(dir, conn) { dir.sayArriving(getNominalTraveler()); }
        sayDepartingDir(dir, conn) { dir.sayDeparting(getNominalTraveler()); }

        /* arrival/departure via a ThroughPassage */
        sayArrivingThroughPassage(conn)
        {
            gLibMessages.sayArrivingThroughPassage(getNominalTraveler(), conn);
        }
        sayDepartingThroughPassage(conn)
        {
            gLibMessages.sayDepartingThroughPassage(getNominalTraveler(), conn);
        }

        /* arrival/departure via a PathPassage */
        sayArrivingViaPath(conn)
            { gLibMessages.sayArrivingViaPath(getNominalTraveler(), conn); }
        sayDepartingViaPath(conn)
            { gLibMessages.sayDepartingViaPath(getNominalTraveler(), conn); }

        /* arrival/departure up/down stairs */
        sayArrivingUpStairs(conn)
            { gLibMessages.sayArrivingUpStairs(getNominalTraveler(), conn); }
        sayArrivingDownStairs(conn)
            { gLibMessages.sayArrivingDownStairs(getNominalTraveler(), conn); }
        sayDepartingUpStairs(conn)
            { gLibMessages.sayDepartingUpStairs(getNominalTraveler(), conn); }
        sayDepartingDownStairs(conn)
            { gLibMessages.sayDepartingDownStairs(getNominalTraveler(), conn); }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A Traveler is an object that can travel.  The two main kinds of
     *   travelers are Actors and Vehicles.  A vehicle can contain multiple
     *   actors, so a single vehicle travel operation could move several
     *   actors.
     *
     *   This class is intended to be multiply inherited, since it is not based
     *   on any simulation object class.
     */
    class Traveler: TravelMessageHandler
        /*
         *   Check, using pre-condition rules, that the traveler is directly
         *   in the given room.  We'll attempt to implicitly remove the actor
         *   from any nested rooms within the given room.
         */
        checkDirectlyInRoom(dest, allowImplicit)
        {
            /* ask the destination to ensure the traveler is in it directly */
            return dest.checkTravelerDirectlyInRoom(self, allowImplicit);
        }

        /*
         *   Check, using pre-condition rules, that the traveler is in the
         *   given room, moving the traveler to the room if possible.
         */
        checkMovingTravelerInto(room, allowImplicit)
        {
            /* subclasses must override */
        }

        /*
         *   Get the travel preconditions specific to the traveler, for the
         *   given connector.  By default, we return no extra conditions.
         */
        travelerPreCond(conn) { return []; }

        /*
         *   Can the traveler travel via the given connector to the given
         *   destination?  Returns true if the travel is permitted, nil if not.
         *
         *   By default, this simply returns true to indicate that the travel
         *   is allowed.  Individual instances can override this to enforce
         *   limitations on what kind of travel the traveler can perform.
         */
        canTravelVia(connector, dest) { return true; }

        /*
         *   Explain why the given travel is not possible.  This is called when
         *   canTravelVia() returns nil, to display a report to the player
         *   explaining why the travel was disallowed.
         *
         *   By default, we do nothing, since our default canTravelVia() never
         *   disallows any travel.  If canTravelVia() is overridden to disallow
         *   travel under some conditions, this must be overridden to generate
         *   an appropriate explanatory report.
         */
        explainNoTravelVia(connector, dest) { }

        /*
         *   Show my departure message - this is shown when I'm leaving a room
         *   where the player character can see me for another room where the
         *   player character cannot see me.
         */
        describeDeparture(dest, connector)
        {
            /*
             *   If we're visible to the player character, describe the
             *   departure.  Ask the connector to describe the travel, since
             *   it knows the direction of travel and can describe special
             *   things like climbing stairs.
             *
             *   Don't bother to describe the departure if the traveler is the
             *   player character, or the player character is inside the
             *   traveler.  The PC obviously can't leave the presence of
             *   itself.
             */
            if (!isActorTraveling(gPlayerChar))
            {
                /*
                 *   invoke the departure with a visual sense context for the
                 *   traveler
                 */
                callWithSenseContext(self, sight,
                                     {: describeNpcDeparture(dest, connector)});
            }
        }

        /*
         *   Describe the departure of a non-player character, or any traveler
         *   not involving the player character.
         */
        describeNpcDeparture(dest, connector)
        {
            /*
             *   If the PC can see our destination, we're doing "local travel"
             *   - we're traveling from one top-level location to another, but
             *   entirely in view of the PC.  In this case, the PC was already
             *   aware of our presence, so we don't want to describe the
             *   departure as though we're actually leaving; we're just moving
             *   around.  If the destination isn't in sight, then we're truly
             *   departing.
             */
            if (gPlayerChar.canSee(dest))
            {
                /*
                 *   We're moving from somewhere within the PC's sight to
                 *   another location within the PC's sight, so we're not
                 *   actually departing, from the PC's point of view: we're
                 *   just moving around a bit.
                 *
                 *   - If we're moving further away from the PC - that is, if
                 *   we're moving out of the PC's own top-level location and
                 *   into a different top-level location, describe this as a
                 *   "local departure."
                 *
                 *   - If we're moving closer to the PC - moving from a top
                 *   level location that doesn't contain the PC to the PC's
                 *   current top-level location - say nothing.  We'll leave it
                 *   to the "local arrival" message to mention it, because
                 *   that's the more relevant aspect of the travel from the
                 *   PC's POV: we're approaching the PC.
                 *
                 *   - If we're moving from one top-level location to another,
                 *   say nothing.  We'll leave this to arrival time as well.
                 *   Whether we describe remote-to-remote travel as arrival or
                 *   departure is just arbitrary; we only want to generate one
                 *   message, but there's no general reason to prefer
                 *   generating the message at departure vs arrival.
                 */
                if (gPlayerChar.isIn(getOutermostRoom()))
                {
                    /*
                     *   we're moving further away from the player - describe
                     *   this as a local departure
                     */
                    connector.describeLocalDeparture(self, location, dest);
                }
            }
            else
            {
                /* we're really departing - let the connector describe it */
                connector.describeDeparture(self, location, dest);
            }
        }

        /*
         *   Show my arrival message - this is shown when I'm entering a room
         *   in view of the player character from a location where the player
         *   character could not see me.  'backConnector' is the connector in
         *   the destination location from which we appear to be emerging.
         */
        describeArrival(origin, backConnector)
        {
            /*
             *   If the player character is participating in the travel,
             *   describe the arrival from the point of view of the player
             *   character by showing the "look around" description of the new
             *   location.
             *
             *   Otherwise, describe the NPC traveler's arrival.  Only
             *   describe the arrival if the PC can see the traveler, for
             *   obvious reasons.
             */
            if (isActorTraveling(gPlayerChar))
            {
                /*
                 *   Add an intra-report separator, to visually separate the
                 *   description of the arrival from any result text that came
                 *   before.  (For example, if we performed an implied command
                 *   before we could begin travel, this will visually separate
                 *   the results of the implied command from the arrival
                 *   message.)
                 */
                say(gLibMessages.intraCommandSeparator);

                /*
                 *   The player character is traveling - show the travel from
                 *   the PC's perspective.
                 */
                gPlayerChar.lookAround(gameMain.verboseMode.isOn);
            }
            else if (travelerSeenBy(gPlayerChar))
            {
                /*
                 *   The player character isn't traveling, but the PC can see
                 *   me now that I'm in my new location, so describe the
                 *   arrival of the traveler.  Do this within a visual sense
                 *   context for the traveler.
                 */
                callWithSenseContext(
                    self, sight, {: describeNpcArrival(origin, backConnector)});
            }
        }

        /*
         *   Describe the arrival of a non-player character or any other
         *   traveler that doesn't involve the player character.
         */
        describeNpcArrival(origin, backConnector)
        {
            /*
             *   If we know the connector back, use its arrival message;
             *   otherwise, use a generic arrival message.
             */
            if (backConnector != nil)
            {
                /*
                 *   If the PC can see the origin, we're not actually arriving
                 *   anew; instead, we're simply moving around within the PC's
                 *   field of view.  In these cases, we don't want to use the
                 *   normal arrival message, because we're not truly arriving;
                 *   we could be moving closer to the player, further away from
                 *   the player, or moving laterally from one remote location
                 *   to another.  If the PC can't see the origin, we're truly
                 *   arriving.
                 */
                if (gPlayerChar.canSee(origin))
                {
                    /*
                     *   We're not arriving anew, but just moving around within
                     *   the PC's field of view.
                     *
                     *   - If we're moving closer to the PC - moving from a
                     *   top-level location that doesn't contain the PC to one
                     *   that does - describe this as a "local arrival."
                     *
                     *   - If we're moving further away from the PC - from
                     *   within the PC's top-level location to a different
                     *   top-level location - don't say anything.  We will have
                     *   already described this as a "local departure" during
                     *   the departure message phase, and from the PC's point
                     *   of view, that fully covers it.
                     *
                     *   - If we're moving laterally - from one remote
                     *   top-level location to another ("remote" means "does
                     *   not contain the PC") - it's arbitrary whether this
                     *   should be described at arrival or departure time.  We
                     *   arbitrarily pick arrival, so describe the lateral
                     *   local travel now.
                     */
                    if (gPlayerChar.isIn(getOutermostRoom()))
                    {
                        /*
                         *   We're now in the same top-level location as the
                         *   PC, so we've moved closer to the PC, so describe
                         *   this as a "local arrival."
                         */
                        backConnector.describeLocalArrival(
                            self, origin, location);
                    }
                    else if (!gPlayerChar.isIn(origin))
                    {
                        /*
                         *   We're *not* in the same top-level location as the
                         *   PC, and we weren't on departure either, so this is
                         *   a lateral move - a move from one remote location
                         *   to another.  Describe it as such now.
                         */
                        backConnector.describeRemoteTravel(
                            self, origin, location);
                    }
                }
                else
                {
                    /* we're arriving anew - let the connector describe it */
                    backConnector.describeArrival(self, origin, location);
                }
            }
            else
            {
                /* there's no back-connector, so use a generic arrival message */
                gLibMessages.sayArriving(self);
            }
        }

        /*
         *   Travel to a new location.  Moves the traveler to a new location
         *   via the given connector, triggering any side effects of the
         *   travel.
         *
         *   Note that this routine is not normally called directly; in most
         *   cases, the actor's travelTo is called, and it in turn invokes
         *   this method in the appropriate traveler.
         *
         *   'dest' is the new location to which we're traveling.  'connector'
         *   is the TravelConnector we're traversing from the source location
         *   to reach the new destination; the connector is normally the
         *   invoker of this routine.  'backConnector' is the connector in the
         *   destination from which the actor will appear to emerge on the
         *   other end of the travel.
         */
        travelerTravelTo(dest, connector, backConnector)
        {
            local origin;
            local isDescribed;
            local actors;

            /*
             *   Remember my departure original location - this is the
             *   location where the traveler was before we moved.
             */
            origin = location;

            /*
             *   Determine if we're describing the travel.  We'll describe it
             *   if we're actually changing locations, or if the connector is
             *   explicitly a "circular" passage, which means that it connects
             *   back to the same location but involves a non-trivial passage,
             *   as sometimes happens in settings like caves.
             */
            isDescribed = (dest != origin || connector.isCircularPassage);

            /*
             *   Send a before-departure notification to everyone connected to
             *   by containment to the traveler.
             */
            getNotifyTable().forEachAssoc(
                {obj, val: obj.beforeTravel(self, connector)});

            /* notify the actor initiating the travel */
            if (gActor != nil)
                gActor.actorTravel(self, connector);

            /* tell the old room we're leaving, if we changed locations */
            if (origin != nil && isDescribed)
                origin.travelerLeaving(self, dest, connector);

            /* notify the connector that we're traversing it */
            connector.noteTraversal(self);

            /* get the list of traveling actors */
            actors = getTravelerActors();

            /* move to the destination */
            moveIntoForTravel(dest);

            /*
             *   We've successfully completed the travel, so remember it for
             *   each actor involved in the travel.  Do this only if we
             *   actually went somewhere, or if the connector is circular - if
             *   we didn't end up going anywhere, and the connector isn't
             *   circular, we must have turned back halfway without completing
             *   the travel.
             */
            if (dest != origin || connector.rememberCircularPassage)
            {
                foreach (local cur in actors)
                {
                    /* ask the actor to remember the travel */
                    cur.rememberTravel(origin, dest, backConnector);

                    /* remember the destination of the connector for this actor */
                    connector.rememberTravel(origin, cur, dest);

                    /*
                     *   If there's a back-connector, also remember travel in
                     *   the other direction.  The actor can reasonably be
                     *   expected to assume that the connector through which
                     *   it's arriving connects back to the source location, so
                     *   remember the association.
                     */
                    if (backConnector != nil)
                        backConnector.rememberTravel(dest, cur, origin);
                }
            }

            /*
             *   Recalculate the global sense context for message generation
             *   purposes, since we've moved to a new location.  It's possible
             *   that we've arrived at the player character's location, in
             *   which case we might have been suppressing messages (due to
             *   our being in a different room) but should now show messages
             *   (because we're now near the player character).
             */
            if (gAction != nil)
                gAction.recalcSenseContext();

            /* tell the new room we're arriving, if we changed locations */
            if (location != nil && isDescribed)
                location.travelerArriving(self, origin, connector, backConnector);

            /* notify objects now connected by containment of the arrival */
            getNotifyTable().forEachAssoc(
                {obj, val: obj.afterTravel(self, connector)});
        }

        /*
         *   Perform "local" travel - that is, travel between nested rooms
         *   within a single top-level location.  By default, we simply defer
         *   to the actor to let it perform the local travel.
         */
        travelerTravelWithin(actor, dest)
        {
            actor.travelerTravelWithin(actor, dest);
        }

        /*
         *   Get a lookup table giving the set of objects to be notified of a
         *   beforeTravel/afterTravel event.  By default, we return a table
         *   including every object connected to the traveler by containment.
         */
        getNotifyTable()
        {
            /* return the table of objects connected by containment */
            return connectionTable();
        }

        /*
         *   Determine if the given actor can see this traveler.  By default,
         *   we'll simply check to see if the actor can see 'self'.
         */
        travelerSeenBy(actor) { return actor.canSee(self); }

        /*
         *   Is the given actor traveling with this traveler?  Returns true if
         *   the actor is in my getTravelerActors list.
         */
        isActorTraveling(actor)
        {
            /* check to see if the given actor is in my actor list */
            return getTravelerActors.indexOf(actor) != nil;
        }

        /*
         *   Is the given object being carried by the traveler?  Returns true
         *   if the object is inside the traveler itself, or is inside any of
         *   the actors traveling.
         */
        isTravelerCarrying(obj)
        {
            /* if the object is inside the traveler, it's being carried */
            if (obj.isIn(self))
                return true;

            /* if the object is inside any traveling actor, it's being carried */
            foreach (local cur in getTravelerActors)
            {
                if (obj.isIn(cur))
                    return true;
            }

            /* the object isn't being carried */
            return nil;
        }

        /* invoke a callback function for each traveling actor */
        forEachTravelingActor(func)
        {
            /* by default, get the list, and invoke the callback per item */
            getTravelerActors.forEach(func);
        }

        /*
         *   Get the list of actors taking part in the travel.  When an actor
         *   is the traveler, this list simply contains the actor itself; for
         *   a vehicle or other composite traveler that moves more than one
         *   actor at a time, this should return the list of all of the actors
         *   involved in the travel.
         */
        getTravelerActors = []

        /*
         *   Get the list of actors traveling undo their own power.  In the
         *   case of an actor traveling directly, this is just the actor; in
         *   the case of an actor pushing something, this is likewise the
         *   actor; in the case of a group of actors traveling together, this
         *   is the list of traveling actors; in the case of a vehicle, this
         *   is an empty list, since anyone traveling with the vehicle is
         *   traveling under the vehicle's power.
         */
        getTravelerMotiveActors = []
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A Travel Connector is a special connection interface that allows for
     *   travel from one location to another.  Most actor movement, except for
     *   movement between locations related by containment (such as from a room
     *   to sitting in a chair within the room) are handled through travel
     *   connector objects.
     *
     *   Travel connectors are used in the directional link properties in rooms
     *   - north, south, east, west, in, out, etc.  A room direction link
     *   property is always set to a travel connector - but note that a room is
     *   itself a travel connector, so a travel link in one room can simply be
     *   set to point directly to another room.  In many cases, rooms
     *   themselves serve as travel connectors, so that one room can point a
     *   direction link property directly to another room.
     *
     *   Some travel connectors are physical objects in the simulation, such as
     *   doors or stairways; other connectors are just abstract objects that
     *   represent connections, but don't appear as manipulable objects in the
     *   game.
     *
     *   A travel connector provides several types of information about travel
     *   through its connection:
     *
     *   - For actors actually traveling, the connector provides a method that
     *   moves an actor through the connector.  This method can trigger any
     *   side effects of the travel.
     *
     *   - For automatic map builders, actor scripts, and other callers who
     *   want to learn what can be known about the link without actually
     *   traversing it, the connector provides an "apparent destination"
     *   method.  This method returns the destination of travel through the
     *   connector that a given actor would expect just by looking at the
     *   connector.  The important thing about this routine is that it doesn't
     *   trigger any side effects, but simply indicates whether travel is
     *   apparently possible, and if so what the destination of the travel
     *   would be.
     */
    class TravelConnector: Thing
        /*
         *   Get any connector-specific pre-conditions for travel via this
         *   connector.
         */
        connectorTravelPreCond()
        {
            local lst;

            /* start with no conditions */
            lst = [];

            /* if we have a staging location, require that we're in it */
            if (connectorStagingLocation != nil)
                lst = [new TravelerDirectlyInRoom(gActor, self,
                                                  connectorStagingLocation)];

            /*
             *   If we're a physical Thing with a non-nil location, require
             *   that we be touchable.  Only physical objects need to be
             *   touchable; connectors are sometimes abstract objects, which
             *   obviously can't be touched.
             */
            if (ofKind(Thing) && location != nil)
            {
                /* require that the traveler can touch the connector */
                local cond = new TouchObjCondition(gActor.getTraveler(self));
                lst += new ObjectPreCondition(self, cond);
            }

            /* return the result */
            return lst;
        }

        /*
         *   The "staging location" for travel through this connector.  By
         *   default, if we have a location, that's our staging location; if
         *   we don't have a location (in which case we probably an outermost
         *   room), we don't have a staging location.
         */
        connectorStagingLocation = (location)

        /*
         *   Get the travel preconditions that this connector requires for
         *   travel by the given actor.  In most cases, this won't depend on
         *   the actor, but it's provided as a parameter anyway; in most cases,
         *   this will just apply the conditions that are relevant to actors as
         *   travelers.
         *
         *   By default, we require actors to be "travel ready" before
         *   traversing a connector.  The exact meaning of "travel ready" is
         *   provided by the actor's immediate location, but it usually simply
         *   means that the actor is standing.  This ensures that the actor
         *   isn't sitting in a chair or lying down or something like that.
         *   Some connectors might not require this, so this routine can be
         *   overridden per connector.
         *
         *   Note that this will only be called when an actor is the traveler.
         *   When a vehicle or other kind of traveler is doing the travel, this
         *   will not be invoked.
         */
        actorTravelPreCond(actor)
        {
            /*
             *   create an object precondition ensuring that the actor is
             *   "travel ready"; the object of this precondition is the
             *   connector itself
             */
            return [new ObjectPreCondition(self, actorTravelReady)];
        }

        /*
         *   Barrier or barriers to travel.  This property can be set to a
         *   single TravelBarrier object or to a list of TravelBarrier
         *   objects.  checkTravelBarriers() checks each barrier specified
         *   here.
         */
        travelBarrier = []

        /*
         *   Check barriers.  The TravelVia check() routine must call this to
         *   enforce barriers.
         */
        checkTravelBarriers(dest)
        {
            local traveler;
            local lst;

            /* get the traveler */
            traveler = gActor.getTraveler(self);

            /* ask the traveler what it thinks of travel through this connector */
            if (!traveler.canTravelVia(self, dest))
            {
                /* explain why the traveler can't pass */
                traveler.explainNoTravelVia(self, dest);

                /* terminate the command */
                exit;
            }

            /* check any travel conditions we apply directly */
            if (!canTravelerPass(traveler))
            {
                /* explain why the traveler can't pass */
                explainTravelBarrier(traveler);

                /* terminate the command */
                exit;
            }

            /* get the barrier list */
            lst = travelBarrier;

            /* if it's just a single object, make it a list of one element */
            if (!lst.ofKind(Collection))
                lst = [lst];

            /* check each item in our barrier list */
            foreach (local cur in lst)
            {
                /* if this barrier doesn't allow travel, we cannot travel */
                if (!cur.canTravelerPass(traveler))
                {
                    /* ask the barrier to explain why travel isn't possible */
                    cur.explainTravelBarrier(traveler);

                    /* terminate the command */
                    exit;
                }
            }
        }

        /*
         *   Check to see if the Traveler object is allowed to travel through
         *   this connector.  Returns true if travel is allowed, nil if not.
         *
         *   This is called from checkTravelBarriers() to check any conditions
         *   coded directly into the TravelConnector.  By default, we simply
         *   return true; subclasses can override this to apply special
         *   conditions.
         *
         *   If an override wants to disallow travel, it should return nil
         *   here, and then provide an override for explainTravelBarrier() to
         *   provide a descriptive message explaining why the travel isn't
         *   allowed.
         *
         *   Conditions here serve essentially the same purpose as barrier
         *   conditions.  The purpose of providing this additional place for
         *   the same type of conditions is simply to improve the convenience
         *   of defining travel conditions for cases where barriers are
         *   unnecessary.  The main benefit of using a barrier is that the same
         *   barrier object can be re-used with multiple connectors, so if the
         *   same set of travel conditions apply to several different
         *   connectors, barriers allow the logic to be defined once in a
         *   single barrier object and then re-used easily in each place it's
         *   needed.  However, when a particular condition is needed in only
         *   one place, creating a barrier to represent the condition is a bit
         *   verbose; in such cases, the condition can be placed in this method
         *   more conveniently.
         */
        canTravelerPass(traveler) { return true; }

        /*
         *   Explain why canTravelerPass() returned nil.  This is called to
         *   display an explanation of why travel is not allowed by
         *   self.canTravelerPass().
         *
         *   Since the default canTravelerPass() always allows travel, the
         *   default implementation of this method does nothing.  Whenever
         *   canTravelerPass() is overridden to return nil, this should also be
         *   overridden to provide an appropriate explanation.
         */
        explainTravelBarrier(traveler) { }

        /*
         *   Is this connector listed?  This indicates whether or not the exit
         *   is allowed to be displayed in lists of exits, such as in the
         *   status line or in "you can't go that way" messages.  By default,
         *   all exits are allowed to appear in listings.
         *
         *   Note that this indicates if listing is ALLOWED - it doesn't
         *   guarantee that listing actually occurs.  A connector can be
         *   listed only if this is true, AND the point-of-view actor for the
         *   listing can perceive the exit (which means that
         *   isConnectorApparent must return true, and there must be
         *   sufficient light to see the exit).
         */
        isConnectorListed = true

        /*
         *   Get an unlisted proxy for this connector.  This is normally
         *   called from the asExit() macro to set up one room exit direction
         *   as an unlisted synonym for another.
         */
        createUnlistedProxy() { return new UnlistedProxyConnector(self); }

        /*
         *   Determine if the travel connection is apparent - as a travel
         *   connector - to the actor in the given origin location.  This
         *   doesn't indicate whether or not travel is possible, or where
         *   travel goes, or that the actor can tell where the passage goes;
         *   this merely indicates whether or not the actor should realize
         *   that the passage exists at all.
         *
         *   A closed door, for example, would return true, because even a
         *   closed door makes it clear that travel is possible in the
         *   direction, even if it's not possible currently.  A secret door,
         *   on the other hand, would return nil while closed, because it
         *   would not be apparent to the actor that the object is a door at
         *   all.
         */
        isConnectorApparent(origin, actor)
        {
            /* by default, passages are apparent */
            return true;
        }

        /*
         *   Determine if the travel connection is passable by the given
         *   traveler in the current state.  For example, a door would return
         *   true when open, nil when closed.
         *
         *   This information is intended to help game code probing the
         *   structure of the map.  This information is NOT used in actor
         *   travel; for actor travel, we rely on custom checks in the
         *   connector's TravelVia handler to enforce the conditions of travel.
         *   Actor travel uses TravelVia customizations rather than this method
         *   because that allows better specificity in reporting failures.
         *   This method lets game code get at the same information, but in a
         *   more coarse-grained fashion.
         */
        isConnectorPassable(origin, traveler)
        {
            /* by default, we're passable */
            return true;
        }

        /*
         *   Get the apparent destination of travel by the actor to the given
         *   origin.  This returns the location to which the connector
         *   travels, AS FAR AS THE ACTOR KNOWS.  If the actor does not know
         *   and cannot tell where the connector leads, this should return nil.
         *
         *   Note that this method does NOT necessarily return the actual
         *   destination, because we obviously can't know the destination for
         *   certain until we traverse the connection.  Rather, the point of
         *   this routine is to return as much information as the actor is
         *   supposed to have.  This can be used for purposes like
         *   auto-mapping, where we'd want to show what the player character
         *   knows of the map, and NPC goal-seeking, where an NPC tries to
         *   figure out how to get from one point to another based on the
         *   NPC's knowledge of the map.  In these sorts of applications, it's
         *   important to use only knowledge that the actor is supposed to
         *   have within the parameters of the simulation.
         *
         *   Callers should always test isConnectorApparent() before calling
         *   this routine.  This routine does not check to ensure that the
         *   connector is apparent, so it could return misleading information
         *   if used independently of isConnectorApparent(); for example, if
         *   the connector *formerly* worked but has now disappeared, and the
         *   actor has a memory of the former destination, we'll return the
         *   remembered destination.
         *
         *   The actor can know the destination by a number of means:
         *
         *   1.  The location is familiar to the character.  For example, if
         *   the setting is the character's own house, the character would
         *   obviously know the house well, so would know where you'd end up
         *   going east from the living room or south from the kitchen.  We
         *   use the origin method actorKnowsDestination() to determine this.
         *
         *   2.  The destination is readily visible from the origin location,
         *   or is clearly marked.  For example, in an outdoor setting, it
         *   might be clear that going east from the field takes you to the
         *   hilltop.  In an indoor setting, an open passage might make it
         *   clear that going east from the living room takes you to the
         *   dining room.  We use the origin method actorKnowsDestination() to
         *   determine this.
         *
         *   3. The actor has been through the connector already in the course
         *   of the game, and so remembers the connection by virtue of recent
         *   experience.  If our travelMemory class property is set to a
         *   non-nil lookup table object, then we'll automatically use the
         *   lookup table to remember the destination each time an actor
         *   travels via a connector, and use this information by default to
         *   provide apparent destination information.
         */
        getApparentDestination(origin, actor)
        {
            local dest;

            /*
             *   Ask the origin if the actor knows the destination for the
             *   given connector.  If so, and we can determine our
             *   destination, then return the destination.
             */
            if (origin.actorKnowsDestination(actor, self)
                && (dest = getDestination(origin,
                                          actor.getTraveler(self))) != nil)
                return dest;

            /*
             *   If we have a travelMemory table, look to see if the traversal
             *   of this actor via this connector from this origin is recorded
             *   in the table, and if so, assume that the destination is the
             *   same as it was last time.
             *
             *   Note that we ignore our memory of travel if we never saw the
             *   destination of the travel (which would be the case if the
             *   destination was dark every time we've been there, so we've
             *   never seen any details about the location).
             */
            if (travelMemory != nil
                && (dest = travelMemory[[actor, origin, self]]) != nil
                && actor.hasSeen(dest))
            {
                /* we know the destination from past experience */
                return dest;
            }

            /* we don't know the destination */
            return nil;
        }

        /*
         *   Get our destination, given the traveler and the origin location.
         *
         *   This method is required to return the current destination for the
         *   travel.  If the connector doesn't go anywhere, this should return
         *   nil.  The results of this method must be stable for the extent of
         *   a turn, up until the time travel actually occurs; in other words,
         *   it must be possible to call this routine simply for information
         *   purposes, to determine where the travel will end up.
         *
         *   This method should not trigger any side effects, since it's
         *   necessary to be able to call this method more than once in the
         *   course of a given travel command.  If it's necessary to trigger
         *   side effects when the connector is actually traversed, apply the
         *   side effects in noteTraversal().
         *
         *   For auto-mapping and the like, note that getApparentDestination()
         *   is a better choice, since this method has internal information
         *   that might not be apparent to the characters in the game and thus
         *   shouldn't be revealed through something like an auto-map.  This
         *   method is intended for internal use in the course of processing a
         *   travel action, since it knows the true destination of the travel.
         */
        getDestination(origin, traveler) { return nil; }

        /*
         *   Get the travel connector leading to the given destination from the
         *   given origin and for the given travel.  Return nil if we don't
         *   know a connector leading there.
         *
         *   By default, we simply return 'self' if our destination is the
         *   given destination, or nil if not.
         *
         *   Some subclasses might encapsulate one or more "secondary"
         *   connectors - that is, the main connector might choose among
         *   multiple other connectors.  In these cases, the secondary
         *   connectors typically won't be linked to directions on their own,
         *   so the room can't see them directly - it can only find them
         *   through us, since we're effectively a wrapper for the secondary
         *   connectors.  In these cases, we won't have any single destination
         *   ourself, so getDestination() will have to return nil.  But we
         *   *can* work backwards: given a destination, we can find the
         *   secondary connector that points to that destination.  That's what
         *   this routine is for.
         */
        connectorGetConnectorTo(origin, traveler, dest)
        {
            /* if we go there, return 'self', else return nil */
            return (getDestination(origin, traveler) == dest ? self : nil);
        }

        /*
         *   Note that the connector is being traversed.  This is invoked just
         *   before the traveler is moved; this notification is fired after the
         *   other travel-related notifications (beforeTravel, actorTravel,
         *   travelerLeaving).  This is a good place to display any special
         *   messages describing what happens during the travel, because any
         *   messages displayed here will come after any messages related to
         *   reactions from other objects.
         */
        noteTraversal(traveler)
        {
            /* do nothing by default */
        }

        /*
         *   Service routine: add a memory of a successful traversal of a
         *   travel connector.  If we have a travel memory table, we'll add
         *   the traversal to the table, so that we can find it later.
         *
         *   This is called from Traveler.travelerTravelTo() on successful
         *   travel.  We're called for each actor participating in the travel.
         */
        rememberTravel(origin, actor, dest)
        {
            /*
             *   If we have a travelMemory table, add this traversal.  Store
             *   the destination, keyed by the combination of the actor,
             *   origin, and connector object (i.e., self) - this will allow
             *   us to remember the destination we reached last time if we
             *   want to know where the same route goes in the future.
             */
            if (TravelConnector.travelMemory != nil)
                TravelConnector.travelMemory[[actor, origin, self]] = dest;
        }

        /*
         *   Our "travel memory" table.  If this contains a non-nil lookup
         *   table object, we'll store a record of each successful traversal
         *   of a travel connector here - we'll record the destination keyed
         *   by the combination of actor, origin, and connector, so that we
         *   can later check to see if the actor has any memory of where a
         *   given connector goes from a given origin.
         *
         *   We keep this information by default, which is why we statically
         *   create the table here.  Keeping this information does involve
         *   some overhead, so some authors might want to get rid of this
         *   table (by setting the property to nil) if the game doesn't make
         *   any use of the information.  Note that this table is stored just
         *   once, in the TravelConnector class itself - there's not a
         *   separate table per connector.
         */
        travelMemory = static new LookupTable(256, 512)

        /*
         *   Is this a "circular" passage?  A circular passage is one that
         *   explicitly connects back to its origin, so that traveling through
         *   the connector leaves us where we started.  When a passage is
         *   marked as circular, we'll describe travel through the passage
         *   exactly as though we had actually gone somewhere.  By default, if
         *   traveling through a passage leaves us where we started, we assume
         *   that nothing happened, so we don't describe any travel.
         *
         *   Circular passages don't often occur in ordinary settings; these
         *   are mostly useful in disorienting environments, such as twisty
         *   cave networks, where a passage between locations can change
         *   direction and even loop back on itself.
         */
        isCircularPassage = nil

        /*
         *   Should we remember a circular trip through this passage?  By
         *   default, we remember the destination of a passage that takes us
         *   back to our origin only if we're explicitly marked as a circular
         *   passage; in other cases, we assume that the travel was blocked
         *   somehow instead.
         */
        rememberCircularPassage = (isCircularPassage)

        /*
         *   Describe an actor's departure through the connector from the
         *   given origin to the given destination.  This description is from
         *   the point of view of another actor in the origin location.
         */
        describeDeparture(traveler, origin, dest)
        {
            local dir;

            /*
             *   See if we can find a direction linked to this connector from
             *   the origin location.  If so, describe the departure using the
             *   direction; otherwise, describe it using the generic departure
             *   message.
             *
             *   Find the connector from the player character's perspective,
             *   because the description we're generating is for the player's
             *   benefit and thus should be from the PC's perspective.
             */
            if ((dir = origin.directionForConnector(self, gPlayerChar)) != nil)
            {
                /*
                 *   We found a direction linked to the connector, so this
                 *   must have been the way they traveled.  Describe the
                 *   departure as being in that direction.
                 *
                 *   Note that it's possible that more than one direction is
                 *   linked to the same connector.  In such cases, we'll just
                 *   take the first link we find, because it's equally
                 *   accurate to say that the actor went in any of the
                 *   directions linked to the same connector.
                 */
                traveler.sayDepartingDir(dir, self);
            }
            else
            {
                /*
                 *   We didn't find any direction out of the origin linking to
                 *   this connector, so we don't know how what direction they
                 *   went.  Show the generic departure message.
                 */
                traveler.sayDeparting(self);
            }
        }

        /*
         *   Describe an actor's arrival through the connector from the given
         *   origin into the given destination.  This description is from the
         *   point of view of another actor in the destination.
         *
         *   Note that this is called on the connector that reverses the
         *   travel, NOT on the connector the actor is actually traversing -
         *   that is, 'self' is the backwards connector, leading from the
         *   destination back to the origin location.  So, if we have two
         *   sides to a door, and the actor traverses the first side, this
         *   will be called on the second side - the one that links the
         *   destination back to the origin.
         */
        describeArrival(traveler, origin, dest)
        {
            local dir;

            /*
             *   See if we can find a direction linked to this connector in
             *   the destination location.  If so, describe the arrival using
             *   the direction; otherwise, describe it using a generic arrival
             *   message.
             *
             *   Find the connector from the player character's perspective,
             *   because the description we're generating is for the player's
             *   benefit and thus should be from the PC's perspective.
             */
            if ((dir = dest.directionForConnector(self, gPlayerChar)) != nil)
            {
                /*
                 *   we found a direction linked to this back connector, so
                 *   describe the arrival as coming from the direction we
                 *   found
                 */
                traveler.sayArrivingDir(dir, self);
            }
            else
            {
                /*
                 *   we didn't find any direction links, so use a generic
                 *   arrival message
                 */
                traveler.sayArriving(self);
            }
        }

        /*
         *   Describe a "local departure" via this connector.  This is called
         *   when a traveler moves around entirely within the field of view of
         *   the player character, and move *further away* from the PC - that
         *   is, the traveler's destination is visible to the PC when we're
         *   leaving our origin, AND the origin's top-level location contains
         *   the PC.  We'll describe the travel not in terms of truly
         *   departing, but simply in terms of moving away.
         */
        describeLocalDeparture(traveler, origin, dest)
        {
            /* say that we're departing locally */
            traveler.sayDepartingLocally(dest, self);
        }

        /*
         *   Describe a "local arrival" via this connector.  This is called
         *   when the traveler moves around entirely within the field of view
         *   of the player character, and comes *closer* to the PC - that is,
         *   the traveler's origin is visible to the player character when we
         *   arrive in our destination, AND the destination's top-level
         *   location contains the PC.  We'll describe the travel not in terms
         *   of truly arriving, since the traveler was already here to start
         *   with, but rather as entering the destination, but just in terms of
         *   moving closer.
         */
        describeLocalArrival(traveler, origin, dest)
        {
            /* say that we're arriving locally */
            traveler.sayArrivingLocally(dest, self);
        }

        /*
         *   Describe "remote travel" via this connector.  This is called when
         *   the traveler moves around entirely within the field of view of the
         *   PC, but between two "remote" top-level locations - "remote" means
         *   "does not contain the PC."  In this case, the traveler isn't
         *   arriving or departing, exactly; it's just moving laterally from
         *   one top-level location to another.
         */
        describeRemoteTravel(traveler, origin, dest)
        {
            /* say that we're traveling laterally */
            traveler.sayTravelingRemotely(dest, self);
        }

        /*
         *   Find a connector in the destination location that connects back as
         *   the source of travel from the given connector when traversed from
         *   the source location.  Returns nil if there is no such connector.
         *   This must be called while the traveler is still in the source
         *   location; we'll attempt to find the connector back to the
         *   traveler's current location.
         *
         *   The purpose of this routine is to identify the connector by which
         *   the traveler arrives in the new location.  This can be used, for
         *   example, to generate a connector-specific message describing the
         *   traveler's emergence from the connector (so we can say one thing
         *   if the traveler arrives via a door, and another if the traveler
         *   arrives by climing up a ladder).
         *
         *   By default, we'll try to find a travel link in the destination
         *   that links us back to this same connector, in which case we'll
         *   return 'self' as the connector from which the traveler emerges in
         *   the new location.  Failing that, we'll look for a travel link
         *   whose apparent source is the origin location.
         *
         *   This should be overridden for any connector with an explicit
         *   complementary connector.  For example, it is common to implement a
         *   door using a pair of objects, one representing each side of the
         *   door; in such cases, each door object would simply return its twin
         *   here.  Note that a complementary connector doesn't actually have
         *   to go anywhere, since it's still useful to have a connector back
         *   simply for describing travelers arriving on the connector.
         *
         *   This *must* be overridden when the destination location doesn't
         *   have a simple connector whose apparent source is this connector,
         *   because in such cases we won't be able to find the reverse
         *   connector with our direction search.
         */
        connectorBack(traveler, dest)
        {
            local origin;

            /* if there's no destination, there's obviously no connector */
            if (dest == nil)
                return nil;

            /*
             *   get the origin location - this the traveler's current
             *   immediate container
             */
            origin = traveler.location;

            /*
             *   First, try to find a link back to this same connector - this
             *   will handle simple symmetrical links with the same connector
             *   object shared between two rooms.
             *
             *   We try to find the actual connector before looking for a
             *   connector back to the origin - it's possible that there are
             *   several ways back to the starting point, and we want to make
             *   sure we pick the one that was actually traversed if possible.
             */
            foreach (local dir in Direction.allDirections)
            {
                /*
                 *   If this direction link from the destination is linked back
                 *   to this same connector, we have a symmetrical connection,
                 *   so we're the connector back.  Note that we're interested
                 *   only in map structure here, so we don't pass an actor; the
                 *   actor isn't actually in the new location, so what the
                 *   actor can see is irrelevant to us here.
                 */
                if (dest.getTravelConnector(dir, nil) == self)
                {
                    /* the same connector goes in both directions */
                    return self;
                }
            }

            /*
             *   we didn't find a link back to the same connector, so try to
             *   find a link from the destination whose apparent source is the
             *   origin
             */
            foreach (local dir in Direction.allDirections)
            {
                local conn;

                /*
                 *   if this link from the destination has an apparent source
                 *   of our origin, the traveler appears to be arriving from
                 *   this link
                 */
                if ((conn = dest.getTravelConnector(dir, nil)) != nil
                    && conn.fixedSource(dest, traveler) == origin)
                {
                    /*
                     *   this direction has an apparent source of the origin
                     *   room - it's not necessarily the same link they
                     *   traversed, but at least it appears to come from the
                     *   same place they came from, so it'll have to do
                     */
                    return conn;
                }
            }

            /* we couldn't find any link back to the origin */
            return nil;
        }

        /*
         *   Get the "fixed" source for travelers emerging from this connector,
         *   if possible.  This can return nil if the connector does not have a
         *   fixed relationship with another connector.
         *
         *   The purpose of this routine is to find complementary connectors
         *   for simple static map connections.  This is especially useful for
         *   direct room-to-room connections.
         *
         *   When a connector relationship other than a simple static mapping
         *   exists, the connectors must generally override connectorBack(), in
         *   which case this routine will not be needed (at least, this routine
         *   won't be needed as long as the overridden connectorBack() doesn't
         *   call it).  Whenever it is not clear how to implement this routine,
         *   don't - implement connectorBack() instead.
         */
        fixedSource(dest, traveler)
        {
            /* by default, return nothing */
            return nil;
        }

        /*
         *   Can the given actor see this connector in the dark, looking from
         *   the given origin?  Returns true if so, nil if not.
         *
         *   This is used to determine if the actor can travel from the given
         *   origin via this connector when the actor (in the origin location)
         *   is in darkness.
         *
         *   By default, we implement the usual convention, which is that
         *   travel from a dark room is possible only when the destination is
         *   lit.  If we can't determine our destination, we will assume that
         *   the connector is not visible.
         */
        isConnectorVisibleInDark(origin, actor)
        {
            local dest;

            /*
             *   Get my destination - if we can't determine our destination,
             *   then assume we're not visible.
             */
            if ((dest = getDestination(origin, actor.getTraveler(self))) == nil)
                return nil;

            /*
             *   Check the ambient illumination level in the destination.  If
             *   it's 2 or higher, then it's lit; otherwise, it's dark.  If
             *   the destination is lit, consider the connector to be visible,
             *   on the theory that the connector lets a little bit of the
             *   light from the destination leak into the origin room - just
             *   enough to make the connection itself visible without making
             *   anything else in the origin room visible.
             */
            return (dest.wouldBeLitFor(actor));
        }

        /*
         *   Handle travel in the dark.  Specifically, this is called when an
         *   actor attempts travel from one dark location to another dark
         *   location.  (We don't invoke this in any other case:
         *   light-to-light, light-to-dark, and dark-to-light travel are all
         *   allowed without any special checks.)
         *
         *   By default, we will prohibit dark-to-dark travel by calling the
         *   location's darkTravel handler.  Individual connectors can
         *   override this to allow such travel or apply different handling.
         */
        darkTravel(actor, dest)
        {
            /*
             *   by default, simply call the actor's location's darkTravel
             *   handler
             */
            actor.location.roomDarkTravel(actor);
        }

        /*
         *   Action handler for the internal "TravelVia" action.  This is not a
         *   real action, but is instead a pseudo-action that we implement
         *   generically for travel via the connector.  Subclasses that want to
         *   handle real actions by traveling via the connector can use
         *   remapTo(TravelVia) to implement the real action handlers.  Note
         *   that remapTo should be used (rather than, say, asDobjFor), since
         *   this will ensure that every type of travel through the connector
         *   actually looks like a TravelVia action, which is useful for
         *   intercepting travel actions generically in other code.
         */
        dobjFor(TravelVia)
        {
            preCond()
            {
                /*
                 *   For our preconditions, use the traveler's preconditions,
                 *   plus the location's preconditions, plus any special
                 *   connector-specific preconditions we supply.
                 */
                return gActor.getTraveler(self).travelerPreCond(self)
                    + gActor.location.roomTravelPreCond()
                    + connectorTravelPreCond();
            }
            verify()
            {
                /*
                 *   Verify travel for the current command's actor through this
                 *   connector.  This performs normal action verify processing.
                 *
                 *   The main purpose of this routine is to allow the connector
                 *   to flag obviously dangerous travel to allow a caller to
                 *   avoid such travel as an implicit or scripted action.  In
                 *   most cases, there's no need to make travel illogical
                 *   because there's generally no potential ambiguity involved
                 *   in analyzing a travel verb.
                 *
                 *   Note that this routine must check with the actor to
                 *   determine if the actor or a vehicle will actually be
                 *   performing the travel, by calling gActor.getTraveler(), if
                 *   the routine cares about the difference.
                 */
            }
            check()
            {
                local t = gActor.getTraveler(self);
                local dest;

                /*
                 *   Check the travel.
                 *
                 *   This routine should take into account the light levels at
                 *   the source and destination locations, if travel between
                 *   dark rooms is to be disallowed.
                 */

                /* get my destination */
                dest = getDestination(t.location, t);

                /* check dark-to-dark travel */
                gActor.checkDarkTravel(dest, self);

                /* enforce barriers */
                checkTravelBarriers(dest);
            }

            action()
            {
                local t = gActor.getTraveler(self);
                local dest;

                /*
                 *   Execute the travel, moving the command's actor through the
                 *   travel connection: we carry out any side effects of the
                 *   travel and deliver the actor (if appropriate) to the
                 *   destination of the connector.
                 *
                 *   Note that this routine must check with the actor to
                 *   determine if the actor or a vehicle will actually be
                 *   performing the travel, by calling gActor.getTraveler(), if
                 *   the routine cares about the difference.  In most cases,
                 *   the routine won't care: most implementations of this
                 *   routine will (if they effect any travel at all) eventually
                 *   call gActor.travelTo() to carry out the travel, and that
                 *   routine will always route the actual movement to the
                 *   vehicle if necessary.
                 */

                /* get my destination */
                dest = getDestination(t.location, t);

                /* travel to my destination */
                gActor.travelTo(dest, self, connectorBack(t, dest));
            }
        }
    ;

    /*
     *   A "probe" object, for testing light levels in rooms.  This is a dummy
     *   object that we use for what-if testing - it's not actually part of
     *   the simulation.
     */
    lightProbe: Thing;

    /*
     *   A TravelBarrier can be attached to a TravelConnector, via the
     *   travelBarrier property, to form a conditional barrier to travel.
     */
    class TravelBarrier: object
        /*
         *   Determine if this barrier blocks the given traveler.  By default,
         *   we don't block anyone.  This doesn't make us much of a barrier, so
         *   subclasses should override this with a more specific condition.
         */
        canTravelerPass(traveler) { return true; }

        /*
         *   Explain why travel isn't allowed.  This should generate an
         *   appropriate failure report explaining the problem.  This is
         *   invoked when travel is attempted and canTravelerPass returns nil.
         *   Subclasses must override this.
         */
        explainTravelBarrier(traveler) { }
    ;

    /*
     *   An "unlisted proxy" connector acts as a proxy for another connector.
     *   We act exactly like the underlying connector, except that we suppress
     *   the connector from automatic exit lists.  This can be used for cases
     *   where an otherwise normal connector is needed but the connector is
     *   not to appear in automatic exit lists (such as the status line).
     *
     *   The most common situation where this kind of connector is useful is
     *   where multiple directions in a given room all go to the same
     *   destination.  In these cases, it's often desirable for some of the
     *   directions to be unlisted alternatives.  The asExit() macro can be
     *   used for convenience to set up these direction synonyms.
     */
    class UnlistedProxyConnector: object
        construct(pri)
        {
            /* remember my underlying primary connector */
            primaryConn = pri;
        }

        /*
         *   Our underlying connector.  Start out with a default TadsObject
         *   rather than nil in case anyone wants to call a property or test
         *   inheritance before we're finished with our constructor - this will
         *   produce reasonable default behavior without having to test for nil
         *   everywhere.
         */
        primaryConn = TadsObject

        /* we're not listed */
        isConnectorListed = nil

        /* map any TravelVia action to our underlying connector */
        dobjFor(TravelVia) remapTo(TravelVia, primaryConn)

        /* redirect everything we don't handle to the underlying connector */
        propNotDefined(prop, [args]) { return primaryConn.(prop)(args...); }

        /*
         *   As a proxy, we don't want to disguise the fact that we're a proxy,
         *   if someone specifically asks, so admist to being of our own true
         *   kind; but we also act mostly like our underlying connector, so if
         *   someone wants to know if we're one of those, say yes to that as
         *   well.  So, return true if the inherited version returns true, and
         *   also return true if our primary connector would return true.
         */
        ofKind(cls) { return inherited(cls) || primaryConn.ofKind(cls); }
    ;

    /*
     *   A travel connector that doesn't allow any travel - if travel is
     *   attempted, we simply use the origin's cannotTravel method to display
     *   an appropriate message.
     */
    noTravel: TravelConnector
        /* it is obvious that is no passage this way */
        isConnectorApparent(origin, actor) { return nil; }

        /* this is not a passable connector */
        isConnectorPassable(origin, traveler) { return nil; }

        dobjFor(TravelVia)
        {
            /*
             *   we know that no travel will occur, so we don't need to satisfy
             *   any preconditions
             */
            preCond = []

            action()
            {
                /* we can't go this way - use the origin's default message */
                gActor.location.cannotTravel();
            }
        }
    ;

    /*
     *   An "ask which" travel connector.  Rather than just traversing a
     *   connector, we ask for a direct object for a specified travel verb; if
     *   the player supplies the missing indirect object (or if the parser can
     *   automatically choose a default), we'll perform the travel verb using
     *   that direct object.
     *
     *   This type of connector has two uses.
     *
     *   First, the library various instances, with appropriate specified
     *   travel verbs, as the default connector for certain directions that
     *   frequently end up mapping to in-scenario objects.  Specifically,
     *   noTravelIn, noTravelDown, and noTravelOut are used as the default in,
     *   down, and out connectors.  If the player types DOWN, for example, and
     *   there's no override for 'down' in a given room, then we'll invoke
     *   noTravelDown; this will in turn ask for a missing direct object for
     *   the GetOffOf action, since DOWN can mean getting off of a platform or
     *   other nested room when on such a thing.  When there's an obvious
     *   thing to get down from, the parser will provide the default
     *   automatically, which will make DOWN into a simple synonym for GET OFF
     *   OF <whatever>.
     *
     *   Second, games can use this kind of connector for a given direction
     *   when the direction is ambiguous.  For example, you can use this as
     *   the 'north' connector when there are two doors leading north from the
     *   location.  When the player types NORTH, the parser will ask which
     *   door the player wants to go through.
     */
    class AskConnector: TravelConnector, ResolveAsker
        /*
         *   The specific travel action to attempt.  This must be a TAction -
         *   an action that takes a direct object (and only a direct object).
         *   The default is TravelVia, but this should usually be customized
         *   in each instance to the type of travel appropriate for the
         *   possible connectors.
         */
        travelAction = TravelViaAction

        /*
         *   The list of possible direct objects for the travel action.  If
         *   this is nil, we'll simply treat the direct object of the
         *   travelAction as completely missing, forcing the parser to either
         *   find a default or ask the player for the missing object.  If the
         *   travel is limited to a specific set of objects (for example, if
         *   there are two doors leading north, and we want to ask which one
         *   to use), this should be set to the list of possible objects; the
         *   parser will then use the ambiguous noun phrase rules instead of
         *   the missing noun phrase rules to ask the player for more
         *   information.
         */
        travelObjs = nil

        /*
         *   The phrase to use in the disambiguation question to ask which of
         *   the travelObjs entries is to be used.  The language-specific
         *   module provides a suitable default, but this should usually be
         *   overridden if travelObjs is overridden.
         */
        travelObjsPhrase = nil

        /*
         *   An extra prompt message to show before the normal parser prompt
         *   for a missing or ambiguous object.  We'll show this just before
         *   the normal parser message, if it's specified.
         *
         *   If you want to customize the messages more completely, you can
         *   override askDisambig() or askMissingObject().  The parser will
         *   invoke these to generate the prompt, so you can customize the
         *   entire messages by overriding these.
         */
        promptMessage = nil

        /*
         *   For each of the ResolveAsker methods that might be invoked, add
         *   the promptMessage text before the normal parser question.
         */
        askDisambig(targetActor, promptTxt, curMatchList, fullMatchList,
                    requiredNum, askingAgain, dist)
        {
            promptMessage;
            inherited(targetActor, promptTxt, curMatchList, fullMatchList,
                      requiredNum, askingAgain, dist);
        }

        askMissingObject(targetActor, action, which)
        {
            promptMessage;
            inherited(targetActor, action, which);
        }

        /* handle travel via this connector */
        dobjFor(TravelVia)
        {
            /*
             *   No preconditions or checks are necessary, since we don't
             *   actually perform any travel on our own; we simply recast the
             *   command as a new action, hence we want to delegate the
             *   preconditions and check() handling to the replacement action.
             *   Note that this means that you can't put a travel barrier
             *   directly on an AskConnector - you have to put any barriers on
             *   the underlying real connectors instead.
             */
            preCond = []
            check() { }

            /*
             *   Recast the travel into our specified action, asking for the
             *   direct object we need for that action.
             */
            action()
            {
                /*
                 *   if we have a set of possible direct objects, retry this
                 *   with the ambiguous object set; otherwise, retry with a
                 *   completely missing direct object
                 */
                if (travelObjs != nil)
                    travelAction.retryWithAmbiguousDobj(
                        gAction, travelObjs, self, travelObjsPhrase);
                else
                    travelAction.retryWithMissingDobj(gAction, self);
            }
        }

        /*
         *   Get a connector leading to the given destination.  We'll scan our
         *   travel objects; for each one that's a TravelConnector, we'll ask
         *   it to find the connector, and return the result if we get one.
         */
        connectorGetConnectorTo(origin, traveler, dest)
        {
            /* if we have no travel objects, there's nothing to check */
            if (travelObjs == nil)
                return nil;

            /* scan our secondary connectors */
            foreach (local cur in travelObjs)
            {
                /* if this is a travel connector, ask it what it thinks */
                if (cur.ofKind(TravelConnector))
                {
                    local conn;

                    /*
                     *   if this secondary connector can give us a connector to
                     *   the destination, use that connector
                     */
                    conn = cur.connectorGetConnectorTo(origin, traveler, dest);
                    if (conn != nil)
                        return conn;
                }
            }

            /* didn't find a match */
            return nil;
        }
    ;

    /*
     *   A "default ask connector" is an AskConnector that we use for certain
     *   directions (down, in, out) as the library default connector for the
     *   directions.
     */
    class DefaultAskConnector: AskConnector
        /*
         *   since this is a default connector for all locations, indicate that
         *   no travel is apparently possible in this direction
         */
        isConnectorApparent(origin, actor) { return nil; }

        /* this is not a passable connector */
        isConnectorPassable(origin, traveler) { return nil; }
    ;

    /*
     *   A default travel connector for going in.  When travel in the relative
     *   direction "in" isn't allowed, we'll try recasting the command as an
     *   "enter" command with an unknown direct object.
     */
    noTravelIn: DefaultAskConnector
        /* when we go 'in', we'll try to ENTER something */
        travelAction = EnterAction
    ;

    /*
     *   A default travel connector for going out.  When travel in the
     *   relative direction "out" isn't allowed, we'll try recasting the
     *   command as an "get out of" command with an unknown direct object.
     */
    noTravelOut: DefaultAskConnector
        /* when we go 'out', we'll try to GET OUT OF something */
        travelAction = GetOutOfAction
    ;

    /*
     *   A default travel connector for going out from a nested room.  This
     *   works the same way as noTravelOut, except that we'll show OUT as a
     *   listed exit.
     */
    nestedRoomOut: noTravelOut
        isConnectorApparent(origin, actor) { return true; }
    ;

    /*
     *   A special travel connector for 'down' that recasts the command as a
     *   "get off of" command.  This can be used for platforms and the like,
     *   where a 'down' command should usually be taken to mean "get off
     *   platform" rather than "down from enclosing room".
     */
    noTravelDown: DefaultAskConnector
        /* when we go 'down', we'll try to GET OFF OF something */
        travelAction = GetOffOfAction
    ;

    /*
     *   A travel connector for going in that explicitly redirects the command
     *   to "enter" and asks for the missing direct object.  This behaves the
     *   same way as noTravelIn, but explicitly makes the inward travel
     *   apparent; this can be used to override the noTravelIn default for
     *   locations where travel in is explicitly allowed.
     */
    askTravelIn: AskConnector
        travelAction = EnterAction
    ;

    /* explicitly redirect travel out to "get out of" */
    askTravelOut: AskConnector
        travelAction = GetOutOfAction
    ;

    /* explicitly redirect travel down to "get off of" */
    askTravelDown: AskConnector
        travelAction = GetOffOfAction
    ;

    /*
     *   "No Shipboard Directions" travel connector.  This is used as the
     *   default connector for the shipboard directions for the base Room
     *   class.  This connector displays a special message indicating that the
     *   room is not a boat hence the shipboard directions don't work here.
     */
    noShipTravel: noTravel
        dobjFor(TravelVia)
        {
            action()
            {
                /* simply indicate that this direction isn't applicable here */
                gLibMessages.notOnboardShip();
            }
        }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A mix-in class that can be added to objects that also inherit from
     *   TravelConnector to add a message as the connector is traversed.
     *
     *   Note that this isn't itself a travel connector; it's just a class
     *   that should be combined with TravelConnector or one of its
     *   subclasses.  This class should be in the superclass list before the
     *   TravelConnector-derived superclass.
     */
    class TravelWithMessage: object
        /*
         *   My message to display when the player character traverses the
         *   connector.  This should be overridden with the custom message for
         *   the connector.  By default, if we're a Script, we'll invoke the
         *   script to show the next message.
         */
        travelDesc()
        {
            if (ofKind(Script))
                doScript();
        }

        /*
         *   My message to display when any non-player character traverses the
         *   connector.  If this is not overridden, no message will be
         *   displayed when an NPC travels through the connector.
         */
        npcTravelDesc = ""

        /*
         *   Display my message.  By default, we show one message for the
         *   player character and another message for NPC's.
         */
        showTravelDesc()
        {
            if (gActor.isPlayerChar())
                travelDesc;
            else
                npcTravelDesc;
        }

        /* on traversing the connector, show our message */
        noteTraversal(traveler)
        {
            /* display my message */
            showTravelDesc();

            /* inherit any other superclass handling as well */
            inherited(traveler);
        }
    ;

    /*
     *   A simple connector that displays a message when the connector is
     *   traversed.
     */
    class TravelMessage: TravelWithMessage, TravelConnector
        /* my destination location */
        destination = nil

        /* get my destination */
        getDestination(origin, traveler) { return destination; }

        /* our source is the same as our destination */
        fixedSource(dest, traveler) { return destination; }
    ;

    /*
     *   A travel connector that can't be traversed, and which shows a custom
     *   failure message when traversal is attempted.  Instances should define
     *   travelDesc to the message to display when travel is attempted.
     *
     *   Travel is not apparently possible in this direction, so this type of
     *   connector will not show up in automatic exit lists or maps.  This
     *   class is designed for connections that are essentially the same as no
     *   connection at all, but where it's desirable to use a special message
     *   to describe why travel can't be accomplished.
     */
    class NoTravelMessage: TravelMessage
        dobjFor(TravelVia)
        {
            /* as in noTravel, we need no preconditions or checks */
            preCond = []
            action()
            {
                /* simply show my message - that's all we do */
                showTravelDesc();
            }
        }

        /*
         *   Because no travel is possible, we want a non-empty message for
         *   NPC's as well as for the PC; by default, use the same message for
         *   all actors by using travelDesc for NPC's.
         */
        npcTravelDesc = (travelDesc)

        /* travel is not apparently possible in this direction */
        isConnectorApparent(origin, actor) { return nil; }
        isConnectorPassable(origin, traveler) { return nil; }
    ;

    /*
     *   A "fake" connector.  This is a connector that doesn't actually allow
     *   travel, but acts like it *could*.  We simply show a special message
     *   when travel is attempted, but we don't move the actor.
     *
     *   This is a subclass of NoTravelMessage, so instances should customize
     *   travelDes with the special failure message when travel is attempted.
     *
     *   Note that this type of connector is by default *apparently* an exit,
     *   even though it doesn't actually go anywhere.  This is useful for "soft
     *   boundaries," where the game's map is meant to appear to the player to
     *   continue but beyond which no more locations actually exist in the map.
     *   This is an oft-used device meant to create an illusion that the game's
     *   map exists in a larger world even though the larger world is not
     *   modeled.  The message we display should in such cases attribute the
     *   actor's inability to traverse the connector to a suitable constraint
     *   within the context of the game world; for example, the actor could be
     *   said to be unwilling to go beyond this point because the actor knows
     *   or suspects there's nothing for a long way in this direction, or
     *   because the actor's goals require staying within the modeled map, or
     *   because the actor is afraid of what lies beyond.
     *
     *   Note that FakeConnector should only be used when the reason for
     *   blocking the travel is apparent before we even try travel.  In
     *   particular, FakeConnector is good for motivational reasons not to
     *   travel, where the actor decides for reasons of its own not to even
     *   attempt the travel.  It's especially important not to use
     *   FakeConnector in cases where the travel is described as attempted but
     *   aborted halfway in - things like encountering a blocked tunnel.  This
     *   is important because FakeConnector aborts the travel immediately,
     *   before sending out any of the notifications that would accompany
     *   ordinary travel, which means that physical barriers (trolls blocking
     *   the way, being tied to a chair) that would otherwise block ordinary
     *   travel will be bypassed.  For cases where travel is attempted, but
     *   something turns you back halfway in, use DeadEndConnector.
     */
    class FakeConnector: NoTravelMessage
        /*
         *   travel is *apparently* possible in this direction (even though
         *   it's not *actually* possible)
         */
        isConnectorApparent(origin, actor) { return true; }
        isConnectorPassable(origin, traveler) { return nil; }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A Dead End Connector is a connector that appears to lead somewhere,
     *   but which turns out to be impassable for reasons that aren't apparent
     *   until we get some distance into the passage.
     *
     *   The Dead End Connector might look a lot like the Fake Connector, but
     *   there's an important difference.  A Fake Connector is a connector that
     *   can't even be physically attempted: the reason not to take a fake
     *   connector is something that shows up before we even start moving,
     *   usually a motivational reason ("You really can't leave town until you
     *   find your missing brother").  A Dead End Connector, on the other hand,
     *   is meant to model a physical attempt to travel that's blocked by some
     *   problem halfway along, such as travel down a tunnel that turns out to
     *   have caved in.
     */
    class DeadEndConnector: TravelMessage
        /*
         *   The apparent destination name.  If the actor is meant to know the
         *   apparent destination from the outset, or if traversing the
         *   connector gives the actor an idea of where the connector
         *   supposedly goes, this can be used to give the name of that
         *   destination.  This name will show up in exit listings, for
         *   example, once the PC knows where the connector supposedly goes.
         *
         *   Note that this isn't the actual destination of the connector,
         *   since the actual destination is simply back to the origin (that's
         *   the whole point of the dead end, after all).  This is simply where
         *   the connector *appears* to go.  If an attempted traversal doesn't
         *   even reveal that much, then you should just leave this nil, since
         *   the destination will never become apparent to the PC.
         */
        apparentDestName = nil

        /*
         *   Our apparent destination.  By default, we create a FakeDestination
         *   object to represent our apparent destination if we have a non-nil
         *   name for the apparent destination.
         *
         *   If the supposed-but-unreachable destination of the connector is in
         *   fact a real location in the game, you can override this to point
         *   directly to that actual location.  This default is for the typical
         *   case where the supposed destination doesn't actually exist on the
         *   game map as a real room.
         */
        apparentDest()
        {
            /*
             *   if we have an apparent destination name, create a
             *   FakeDestination to represent the apparent destination, and
             *   plug that in as our apparent destination for future reference
             */
            if (apparentDestName != nil)
            {
                local fake;

                /* create a fake destination */
                fake = new FakeDestination(self);

                /* plug it in as our new apparentDest for future calls */
                apparentDest = fake;

                /* return the new object as the result */
                return fake;
            }

            /* our apparent destination is unknown */
            return nil;
        }

        /* get our apparent destination */
        getApparentDestination(origin, actor)
        {
            /*
             *   If the actor knows the destination for the given connector (as
             *   determined by the origin room), or we have a memory of this
             *   traversal, return our fake destination object to represent the
             *   destination.  We can only return our fake destination if we
             *   have an explicit apparent destination, since it's only in this
             *   case that our apparent destination ever actually becomes
             *   known, even after an attempted traversal.
             *
             *   Our actual destination is always just the origin, but we
             *   *appear* to have some other destination.  Even though we can
             *   never actually reach that other, apparent destination, we at
             *   least want to give the appearance of going there, which we
             *   provide through our fake destination object.
             */
            if (apparentDest != nil
                && (origin.actorKnowsDestination(actor, self)
                    || travelMemory[[actor, origin, self]] != nil))
                return apparentDest;

            /* our unreachable destination is not apparent */
            return nil;
        }

        /* there's no corresponding connector back for a dead end */
        connectorBack(traveler, dest) { return nil; }

        /* our actual destination is always our origin */
        getDestination(origin, traveler) { return origin; }

        /* do remember circular trips, since that's the only kind we make */
        rememberCircularPassage = true
    ;

    /*
     *   A fake apparent destination, for dead-end connectors.  The dead-end
     *   connector will create an object of this class to represent its
     *   apparent but actually unreachable destination, if it has an apparent
     *   destination name.
     */
    class FakeDestination: object
        /* construct - remember our associated connector */
        construct(conn) { connector = conn; }

        /* get our destination name - this is the name from our connector */
        getDestName(actor, origin) { return connector.apparentDestName; }

        /* our underlying connector (usually a DeadEndConnector) */
        connector = nil
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A direct room-to-room connector.  In most cases, it is not necessary
     *   to create one of these objects, because rooms can serve as their own
     *   connectors.  These objects are needed in certain cases, though, such
     *   as when a room-to-room connection requires a travel barrier.
     */
    class RoomConnector: TravelConnector
        /* the two rooms we link */
        room1 = nil
        room2 = nil

        /*
         *   get the destination, given the origin: this is the one of the two
         *   rooms we link that we're not in now
         */
        getDestination(origin, traveler)
        {
            if (origin == room1)
                return room2;
            else if (origin == room2)
                return room1;
            else
                return nil;
        }

        fixedSource(origin, traveler)
        {
            /*
             *   we're a symmetrical two-way connector, so our source from the
             *   perspective of one room is the same as the destination from
             *   that same perspective
             */
            return getDestination(origin, traveler);
        }

        /*
         *   Get the precondition for this connector.  The normal
         *   TravelConnector rule that the traveler must be in the outbound
         *   connector's location is not meaningful for an abstract room
         *   connector, because this type of connection itself isn't
         *   represented as a physical game object with a location; it's just
         *   an abstract data structure.  This means that we must have a
         *   directional property in the *source* location that points directly
         *   to the destination (i.e., self).
         *
         *   So, the appropriate starting point for room connectors is the
         *   object that contains the connection.  In other words, we must
         *   search for the nearest object enclosing the *traveler* that has a
         *   direction property directly linked to 'self'; that enclosing
         *   container is the required starting location for the travel.
         */
        connectorTravelPreCond()
        {
            /*
             *   Scan upwards from the traveler's location, looking for an
             *   object that has a directional property linked to self.  Only
             *   scan into locations that the actor can see.
             */
            for (local loc = gActor.getTraveler(self).location ;
                 loc != nil && gActor.canSee(loc) ; loc = loc.location)
            {
                /* look for a directional connector directly from 'loc' to us */
                if (loc.localDirectionLinkForConnector(self) != nil)
                {
                    /*
                     *   we're linked from this enclosing location, so this is
                     *   where we have to be before travel
                     */
                    return [new TravelerDirectlyInRoom(gActor, self, loc)];
                }
            }

            /* we couldn't find a link, so apply no condition */
            return [];
        }
    ;

    /*
     *   A one-way room connector.  This works like an ordinary room
     *   connector, but connects only in one direction.  To use this class,
     *   simply define the 'destination' property to point to the room we
     *   connect to.
     */
    class OneWayRoomConnector: RoomConnector
        /* my destination - instances must define this */
        destination = nil

        /* we always have a fixed destination */
        getDestination(origin, traveler) { return destination; }
    ;

    /*
     *   A room "auto-connector".  This is a special subclass of RoomConnector
     *   that can be mixed in to any BasicLocation subclass to make the room
     *   usable as the direct target of a directional property in another room
     *   - so you could say "east = myRoom", for example, without creating any
     *   intermediate connector.
     */
    class RoomAutoConnector: RoomConnector
        /*
         *   Suppose that roomA.north = roomB.  This means that if an actor is
         *   in roomA, and executes a "north" command, we'll execute a
         *   TravelVia action on room B, because the "connector" will be
         *   roomB.  In these cases, the destination of the travel and the
         *   travel connector are one and the same.  So, when the connector is
         *   roomB, the destination of travel is also simply roomB.
         */
        getDestination(origin, traveler)
        {
            /* we are our own destination */
            return self;
        }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Base class for passages between rooms.  This can be used for a
     *   passage that not only connects the rooms but also exists as an object
     *   in its own right within the rooms it connects.
     *
     *   In most cases, two passage objects will exist - one on each side of
     *   the passage, so one object in each room connected.  One of the
     *   objects should be designated as the "master"; the other is the
     *   "slave."  The master object is the one which should implement all
     *   special behavior involving state changes (such as opening or closing
     *   a door).
     *
     *   This basic passage is not designed to be opened and closed; use Door
     *   for a passage that can be opened and closed.
     */
    class Passage: Linkable, Fixture, TravelConnector
        /*
         *   Our destination - this is where the actor ends up when traveling
         *   through the passage (assuming the passage is open).  By default,
         *   we return the "room location" of our other side's container; in
         *   cases where our other side is not directly in our destination
         *   room (for example, the other side is part of some larger object
         *   structure), this property should be overridden to specify the
         *   actual destination.
         *
         *   If our otherSide is nil, the passage doesn't go anywhere.  This
         *   can be useful to create things that look and act like passages,
         *   but which don't actually go anywhere - in other words,
         *   passage-like decorations.
         */
        destination = (otherSide != nil ? otherSide.location.roomLocation : nil)

        /* get my destination - just return my 'destination' property */
        getDestination(origin, traveler) { return destination; }

        /* get our open/closed status */
        isOpen()
        {
            /*
             *   if we have a separate master object, defer to it; otherwise,
             *   use our own status
             */
            return (masterObject == self ? isOpen_ : masterObject.isOpen);
        }

        /* internal open/closed status - open by default */
        isOpen_ = true

        /*
         *   We're not visible in the dark if we're closed.  If we're open,
         *   the normal rules apply.
         *
         *   Normally, a passage is visible in the dark if there's light in
         *   the adjoining location: our model is that enough light is leaking
         *   in through the passage to make the passage itself visible, but
         *   not enough to light anything else in the current room.  In the
         *   case of a closed passage, though, we assume that it completely
         *   blocks any light from the other room, eliminating any indication
         *   of a passage.
         *
         *   If you do want an openable passage to be visible in the dark even
         *   when it's closed, it's probably better to make the passage
         *   self-illuminating (i.e., with brightness 1), because this will
         *   put the passage in scope and thus allow it to be manipulated.
         */
        isConnectorVisibleInDark(origin, actor)
            { return isOpen() && inherited(origin, actor); }

        /* a passage is passable when it's open */
        isConnectorPassable(origin, traveler) { return isOpen(); }

        /*
         *   Initialize.  If we're a slave, we'll set up the otherSide
         *   relationship between this passage and our master passage.
         */
        initializeThing()
        {
            /* inherit default handling */
            inherited();

            /*
             *   if we have a master side, initialize our relationship with
             *   the master side
             */
            if (masterObject != self)
            {
                /* set our otherSide to point to the master */
                otherSide = masterObject;

                /* set the master's otherSide to point to us */
                masterObject.initMasterObject(self);
            }
        }

        /*
         *   Initialize the master object.  The other side of a two-sided door
         *   will call this on the master object to let the master object know
         *   about the other side.  'other' is the other-side object.  By
         *   default, we'll simply remember the other object in our own
         *   'otherSide' property.
         */
        initMasterObject(other) { otherSide = other; }

        /*
         *   Our corresponding passage object on the other side of the
         *   passage.  This will be set automatically during initialization
         *   based on the masterObject property of the slave - it is not
         *   generally necessary to set this manually.
         */
        otherSide = nil

        /* our other side is the other facet of the passage */
        getFacets() { return otherSide != nil ? [otherSide] : inherited(); }

        /*
         *   our source is always our destination, since we have a one-to-one
         *   relationship with our comlementary passage in the destination
         *   location (we point to it, it points to us)
         */
        fixedSource(origin, traveler) { return destination; }

        /* the connector back is our complementary side, if we have one */
        connectorBack(traveler, dest)
        {
            /* if we have a complementary side, it's the connector back */
            if (otherSide != nil)
                return otherSide;

            /* we don't have a complementary side, so use the default handling */
            return inherited(traveler, dest);
        }

        /* can the given actor travel through this passage? */
        canActorTravel(actor)
        {
            /*
             *   by default, the actor can travel through the passage if and
             *   only if the passage is open
             */
            return isOpen;
        }

        /*
         *   Display our message when we don't allow the actor to travel
         *   through the passage because the passage is closed.  By default,
         *   we'll simply display the default cannotTravel message for the
         *   actor's location, but this can be overridden to provide a more
         *   specific report of the problem.
         */
        cannotTravel()
        {
            /* use the actor's location's cannotTravel handling */
            gActor.location.cannotTravel();
        }

        /* carry out travel via this connector */
        dobjFor(TravelVia)
        {
            /* check travel */
            check()
            {
                /*
                 *   Move the actor only if we're open; if we're not, use the
                 *   standard no-travel handling for the actor's location.
                 *   Note that we don't try to implicitly open the passage,
                 *   because the basic passage is not openable; if we're
                 *   closed, it means we're impassable for some reason that
                 *   presumably cannot be remedied by a simple "open" command.
                 */
                if (!canActorTravel(gActor))
                {
                    /* we're closed, so use our no-travel handling */
                    cannotTravel();
                    exit;
                }
                else
                {
                    /* inherit the default checks */
                    inherited();
                }
            }
        }

        dobjFor(LookThrough)
        {
            action()
            {
                /*
                 *   if we're open, we simply "can't see much from here";
                 *   otherwise, we can't see through it at all
                 */
                if (isOpen)
                    mainReport(&nothingThroughPassageMsg);
                else
                    inherited();
            }
        }
    ;

    /*
     *   A passage that an actor can travel through (with a "go through" or
     *   "enter" command).  A "go through" command applied to the passage
     *   simply makes the actor travel through the passage as though using the
     *   appropriate directional command.
     *
     *   We describe actors arriving or departing via the passage using
     *   "through" descriptions ("Bob arrives through the door," etc).
     */
    class ThroughPassage: Passage
        describeDeparture(traveler, origin, dest)
        {
            /* describe the traveler departing through this passage */
            traveler.sayDepartingThroughPassage(self);
        }

        describeArrival(traveler, origin, dest)
        {
            /* describe the traveler arriving through this passage */
            traveler.sayArrivingThroughPassage(self);
        }

        /* treat "go through self" as travel through the passage */
        dobjFor(GoThrough) remapTo(TravelVia, self)

        /* "enter" is the same as "go through" for this type of passage */
        dobjFor(Enter) asDobjFor(GoThrough)

        /*
         *   Explicitly map the indirect object 'verify' handlers for the
         *   push-travel commands corresponding to the regular travel commands
         *   we provide.  This isn't strictly necessary except when we're mixed
         *   in to something like a Decoration, in which case explicitly
         *   defining the mapping here is important because it will give the
         *   mapping higher precedence than a catch-all handlers overriding the
         *   same mapping we inherit from Thing.  We use the same mapping that
         *   Thing provides by default.
         */
        mapPushTravelIobj(PushTravelThrough, TravelVia)
        mapPushTravelIobj(PushTravelEnter, TravelVia)
    ;

    /*
     *   A Path Passage is a specialization of through passage that's more
     *   suitable for outdoor locations.  This type of passage is good for
     *   things like outdoor walkways, paths, and streets, where travelers walk
     *   along the connector but aren't enclosed by it.  The main practical
     *   difference is how we describe departures and arrivals; in English, we
     *   describe these as being "via" the path rather than "through" the path,
     *   since there's no enclosure involved in these connections.
     */
    class PathPassage: ThroughPassage
        describeDeparture(traveler, origin, dest)
        {
            traveler.sayDepartingViaPath(self);
        }

        describeArrival(traveler, origin, dest)
        {
            /* describe the traveler arriving through this passage */
            traveler.sayArrivingViaPath(self);
        }

        /* putting something on a path is the same as dropping it */
        iobjFor(PutOn) remapTo(Drop, DirectObject)

        /* use a special message for standing on a path */
        cannotStandOnMsg = &cannotStandOnPathMsg

        /* FOLLOW PATH -> travel via the path */
        dobjFor(Follow) remapTo(TravelVia, DirectObject)
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   The exit portal of a one-way passage.  This isn't a fully functional
     *   passage, but rather an object that acts as the receiving end of a
     *   passage that can only be traversed in one direction.
     *
     *   This can be used for various purposes: the underside of a trap door,
     *   the bottom of a chute, the exit of a wormhole.
     */
    class ExitOnlyPassage: ThroughPassage
        dobjFor(TravelVia)
        {
            action()
            {
                /*
                 *   Show our default failure message.  This can be overridden
                 *   to provide a more customized description of why the
                 *   passage cannot be entered from this side.
                 */
                reportFailure(&cannotEnterExitOnlyMsg, self);
            }
        }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Stairway - this is a special kind of passage that is used for
     *   vertical connections, such as stairways and ladders.  This type of
     *   passage doesn't allow "enter" or "go through," but does allow "climb".
     *
     *   The basic Stairway should generally not be used, as it doesn't know
     *   where it is relative to connected stairs.  Instead, the more specific
     *   StairwayUp and StairwayDown should be used in most cases.
     *
     *   Note that at midpoints along long stairways (landings, for example),
     *   separate stairway objects should normally be used: one going up and
     *   one going down.
     */
    class Stairway: Passage
        /*
         *   Treat "climb self" as travel through the passage
         */
        dobjFor(Climb) remapTo(TravelVia, self)
    ;

    /*
     *   A stairway going up from here.
     */
    class StairwayUp: Stairway
        describeArrival(traveler, origin, dest)
        {
            /*
             *   describe the actor arriving by coming down these stairs (they
             *   leave up, so they arrive down)
             */
            traveler.sayArrivingDownStairs(self);
        }

        describeDeparture(traveler, origin, dest)
        {
            /* describe the actor leaving up these stairs */
            traveler.sayDepartingUpStairs(self);
        }

        /* "climb up" is the specific direction for "climb" here */
        dobjFor(ClimbUp) asDobjFor(Climb)

        /* cannot climb down from here */
        dobjFor(ClimbDown)
        {
            verify() { illogical(&stairwayNotDownMsg); }
        }
    ;

    /*
     *   A stairway going down from here.
     */
    class StairwayDown: Stairway
        /* "climb down" is the specific direction for "climb" here */
        dobjFor(ClimbDown) asDobjFor(Climb)

        describeArrival(traveler, origin, dest)
        {
            /*
             *   describe the actor arriving by coming up these stairs (they
             *   leave down, so they arrive up)
             */
            traveler.sayArrivingUpStairs(self);
        }

        describeDeparture(traveler, origin, dest)
        {
            /* describe the actor traveling down these stairs */
            traveler.sayDepartingDownStairs(self);
        }

        /* cannot climb up from here */
        dobjFor(ClimbUp)
        {
            verify() { illogical(&stairwayNotUpMsg); }
        }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Basic Door class.  This is the base class for door-like objects: a
     *   travel connector with two sides, and which can be opened and closed.
     *   Each side of the door is linked to the other, so that opening or
     *   closing one side makes the same change to the other side.
     *
     *   A basic door has the internal functionality of a door, but doesn't
     *   provide handling for player commands that manipulate the open/closed
     *   status.
     */
    class BasicDoor: BasicOpenable, ThroughPassage
        /*
         *   Open/close the door.  If we have a complementary door object
         *   representing the other side, we'll remark in the sensory context
         *   of its location that it's also opening or closing.
         */
        makeOpen(stat)
        {
            /* inherit the default behavior */
            inherited(stat);

            /*
             *   if our new status is in effect, notify the other side so that
             *   it can generate a message in its location
             */
            if (isOpen == stat && otherSide != nil)
                otherSide.noteRemoteOpen(stat);
        }

        /*
         *   Note a "remote" change to our open/close status.  This is an
         *   open/close operation performed on our complementary object
         *   representing the other side of the door.  We'll remark on the
         *   change in the sensory context of this side, but only if we're
         *   suppressing output in the current context - if we're not, then
         *   the player will see the message generated by the side that we
         *   directly acted upon, so we don't need a separate report for the
         *   other side.
         */
        noteRemoteOpen(stat)
        {
            /*
             *   If I'm not visible to the player character in the current
             *   sense context, where the action is actually taking place,
             *   switch to my own sensory context and display a report.  This
             *   way, if the player can see this door but not the other side,
             *   and the action is taking place on the other side, we'll still
             *   see a note about the change.  We only need to do this if
             *   we're not already visible to the player, because if we are,
             *   we'll generate an ordinary report of the door opening in the
             *   action's context.
             */
            if (senseContext.isBlocking)
            {
                /* show a message in my own sensory context */
                callWithSenseContext(self, sight, {: describeRemoteOpen(stat) });
            }
        }

        /*
         *   Describe the door being opened remotely (that is, by someone on
         *   the other side).  This is called from noteRemoteOpen to actually
         *   display the message.
         */
        describeRemoteOpen(stat)
        {
            /* show the default library message for opening the door remotely */
            gLibMessages.sayOpenDoorRemotely(self, stat);
        }

        /* carry out travel through the door */
        dobjFor(TravelVia)
        {
            action()
            {
                /* move the actor to our destination */
                inherited();

                /* remember that this is the last door the actor traversed */
                gActor.rememberLastDoor(self);
            }
        }

        /*
         *   Boost the likelihood that a command is referring to us if we just
         *   traveled through the door; this is meant to be called from
         *   verify() routines.  For a few commands (close, lock), the most
         *   likely door being referred to is the door just traversed.
         */
        boostLikelihoodOnTravel()
        {
            /*
             *   if this (or our other side) is the last door the actor
             *   traversed, boost the likelihood that they're referring to us
             */
            if (gActor.lastDoorTraversed == self
                || (otherSide != nil && gActor.lastDoorTraversed == otherSide))
                logicalRank(120, 'last door traversed');
        }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Door.  This is a travel connector that can be opened and closed with
     *   player commands.  This is a simple subclass of BasicDoor that adds
     *   support for player commands to manipulate the door.
     */
    class Door: Openable, BasicDoor
        /* make us initially closed */
        initiallyOpen = nil

        /* if we can't travel because the door is closed, say so */
        cannotTravel()
        {
            if (gActor.canSee(self) && !isOpen)
                reportFailure(&cannotGoThroughClosedDoorMsg, self);
            else
                inherited();
        }

        /*
         *   get the 'door open' precondition - by default, we create a
         *   standard doorOpen precondition for this object, but this can be
         *   overridden if desired to create custom doorOpen variations
         */
        getDoorOpenPreCond() { return new ObjectPreCondition(self, doorOpen); }

        /* the door must be open before we can travel this way */
        connectorTravelPreCond()
        {
            local ret;
            local doorCond;

            /* start with the inherited conditions */
            ret = inherited();

            /* if there's a door-open condition, add it as well */
            if ((doorCond = getDoorOpenPreCond()) != nil)
                ret += doorCond;

            /* return the result */
            return ret;
        }

        dobjFor(Close)
        {
            verify()
            {
                /* inherit default handling */
                inherited();

                /* boost the likelihood if they just traveled through us */
                boostLikelihoodOnTravel();
            }
        }

        dobjFor(Lock)
        {
            verify()
            {
                /* inherit default handling */
                inherited();

                /* boost the likelihood if they just traveled through us */
                boostLikelihoodOnTravel();
            }
        }

        /*
         *   looking behind a door implies opening it to see what's on the
         *   other side
         */
        dobjFor(LookBehind)
        {
            preCond = (inherited + objOpen)
            action()
            {
                /*
                 *   If the door is open, AND we implicitly opened the door to
                 *   carry out this action, use a special report that takes
                 *   into account that we were specifically looking to see what
                 *   was on the other side of the door; otherwise, use the
                 *   default behavior
                 */
                if (isOpen
                    && gTranscript.currentActionHasReport(
                        {x: (x.action_.ofKind(OpenAction)
                             && x.isActionImplicit())}))
                    mainReport(&nothingBeyondDoorMsg);
                else
                    inherited();
            }
        }

        /* looking through a door requires it to be open */
        dobjFor(LookThrough)
        {
            preCond = (nilToList(inherited) + objOpen)
        }

    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Secret Door.  This is a special kind of door that gives no hint of
     *   its being a door when it's closed.  This can be used for objects that
     *   serve as a secret passage but are otherwise normal objects, such as a
     *   bookcase that conceals a passage.
     */
    class SecretDoor: BasicDoor
        /* a secret passage usually starts off secret (i.e., closed) */
        initiallyOpen = nil

        isConnectorApparent(origin, actor)
        {
            /*
             *   A secret passage is not apparent as a pasage unless it's
             *   open.
             */
            if (isOpen)
            {
                /* the passage is open - use the inherited handling */
                return inherited(origin, actor);
            }
            else
            {
                /*
                 *   the passage is closed, so it's not apparently a passage
                 *   at all
                 */
                return nil;
            }
        }
    ;

    /*
     *   Hidden Door.  This is a secret door that is invisible when closed.
     *   This can be used when the passage isn't even visible when it's
     *   closed, such as a door that seamlessly melds into the wall.
     */
    class HiddenDoor: SecretDoor
        /*
         *   When we're closed, we're completely invisible, so we have no
         *   sight presence.  When we're open, we have our normal visual
         *   presence.
         */
        sightPresence = (isOpen)
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Automatically closing door.  This type of door closes behind an actor
     *   as the actor traverses the connector.
     */
    class AutoClosingDoor: Door
        dobjFor(TravelVia)
        {
            action()
            {
                /* inherit the default handling */
                inherited();

                /*
                 *   Only close the door if the actor performing the travel
                 *   isn't accompanying another actor.  If the actor is
                 *   accompanying someone, we essentially want them to hold the
                 *   door for the other actor - at the very least, we don't
                 *   want to slam it in the other actor's face!
                 */
                if (!gActor.curState.ofKind(AccompanyingInTravelState))
                {
                    /* close the door */
                    makeOpen(nil);

                    /* mention that the automatic closing */
                    reportAutoClose();
                }
            }
        }

        /*
         *   Report the automatic closure.  The TravelVia action() calls this
         *   after closing the door to generate a message mentioning that the
         *   door was closed.  By default, we just show the standard
         *   doorClosesBehindMsg library message.
         */
        reportAutoClose() { mainReport(&doorClosesBehindMsg, self); }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   The base class for Enterables and Exitables.  These are physical
     *   objects associated with travel connectors.  For example, the object
     *   representing the exterior of a building in the location containing
     *   the building could be an Enterable, so that typing ENTER BUILDING
     *   takes us into the building via the travel connector that leads inside.
     *
     *   Enterables and Exitables are physical covers for travel connectors.
     *   These objects aren't travel connectors themselves, and they don't
     *   specify the destination; instead, these just point to travel
     *   connectors.
     */
    class TravelConnectorLink: object
        /* the underlying travel connector */
        connector = nil

        /*
         *   The internal "TravelVia" action just maps to travel via the
         *   underlying connector.  However, we want to apply our own
         *   preconditions, so we don't directly remap to the underlying
         *   connector.  Instead, we provide our own full TravelVia
         *   implementation, and then we perform the travel on the underlying
         *   connector via a replacement action in our own action() handler.
         */
        dobjFor(TravelVia)
        {
            /* the physical link object has to be touchable */
            preCond = [touchObj]

            verify() { }
            action()
            {
                /* carry out the action by traveling via our connector */
                replaceAction(TravelVia, connector);
            }
        }

        /*
         *   These objects are generally things like buildings (exterior or
         *   interior), which tend to be large enough that their details can be
         *   seen at a distance.
         */
        sightSize = large
    ;

    /*
     *   An Enterable is an object that exists in one location, and which can
     *   be entered to take an actor to another location.  Enterables are used
     *   for things such as the outsides of buildings, so that the building
     *   can have a presence on its outside and can be entered via a command
     *   like "go into building".
     *
     *   An Enterable isn't a connector, but points to a connector.  This type
     *   of object is most useful when there's already a connector that exists
     *   as a separate object, such as the door to a house: the house object
     *   can be made an Enterable that simply points to the door object.
     */
    class Enterable: TravelConnectorLink, Fixture
        /*
         *   "Enter" action this simply causes the actor to travel via the
         *   connector.
         */
        dobjFor(Enter) remapTo(TravelVia, self)

        /* explicitly define the push-travel indirect object mapping */
        mapPushTravelIobj(PushTravelEnter, TravelVia)
    ;

    /*
     *   An Exitable is like an Enterable, except that you exit it rather than
     *   enter it.  This can be used for objects representing the current
     *   location as an enclosure (a jail cell), or an exit door.
     */
    class Exitable: TravelConnectorLink, Fixture
        /* Get Out Of/Exit action - this simply maps to travel via the connector */
        dobjFor(GetOutOf) remapTo(TravelVia, self)

        /* explicitly define the push-travel indirect object mapping */
        mapPushTravelIobj(PushTravelGetOutOf, TravelVia)
    ;

    /*
     *   An EntryPortal is just like an Enterable, except that "go through"
     *   also works on it.  Likewise, an ExitPortal is just like an Exitable
     *   but accepts "go through" as well.
     */
    class EntryPortal: Enterable
        dobjFor(GoThrough) remapTo(TravelVia, self)
    ;
    class ExitPortal: Exitable
        dobjFor(GoThrough) remapTo(TravelVia, self)
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A "TravelPushable" is an object that can't be taken, but can be moved
     *   from one location to another via commands of the form "push obj dir,"
     *   "push obj through passage," and the like.
     *
     *   TravelPushables tend to be rather rare, and we expect that instances
     *   will almost always be special cases that require additional
     *   specialized code.  This is therefore only a general framework for
     *   pushability.
     */
    class TravelPushable: Immovable
        cannotTakeMsg = &cannotTakePushableMsg
        cannotMoveMsg = &cannotMovePushableMsg
        cannotPutMsg = &cannotPutPushableMsg

        /* can we be pushed via the given travel connector? */
        canPushTravelVia(connector, dest) { return true; }

        /* explain why canPushTravelVia said we can't be pushed this way */
        explainNoPushTravelVia(connector, dest) { }

        /*
         *   Receive notification that we're about to be pushed somewhere.
         *   This is called just before the underlying traveler performs the
         *   actual travel.  (By default, we don't even define this method, to
         *   ensure that if we're combined with another base class that
         *   overrides the method, the overrider will be called.)
         */
        // beforeMovePushable(traveler, connector, dest) { }

        /*
         *   Move the object to a new location as part of a push-travel
         *   operation.  By default, this simply uses moveInto to move the
         *   object, but subclasses can override this to apply conditions to
         *   the pushing, put the object somewhere else in some cases, display
         *   extra messages, or do anything else that's necessary.
         *
         *   Our special PushTraveler calls this routine after the underlying
         *   real traveler has finished its travel to the new location, so the
         *   traveler's location will indicate the destination of the travel.
         *   Note that this routine is never called if the traveler ends up in
         *   its original location after the travel, so this routine isn't
         *   called when travel isn't allowed for the underlying traveler.
         */
        movePushable(traveler, connector)
        {
            /* move me to the traveler's new location */
            moveIntoForTravel(traveler.location);

            /* describe what we're doing */
            describeMovePushable(traveler, connector);
        }

        /*
         *   Describe the actor pushing the object into the new location.
         *   This is called from movePushable; we pull this out as a separate
         *   routine so that the description of the pushing can be overridden
         *   without having to override all of movePushable.
         */
        describeMovePushable(traveler, connector)
        {
            /*
             *   If the actor is the player character, mention that we're
             *   pushing the object with us.  For an NPC, show nothing,
             *   because the normal travel message will mention that the NPC
             *   is pushing the object as part of the normal travel report
             *   (thanks to PushTraveler's name override).
             */
            if (gActor.isPlayerChar)
                mainReport(&okayPushTravelMsg, self);
        }

        dobjFor(PushTravel)
        {
            verify() { }
            action()
            {
                local newTrav;
                local oldTrav;
                local wasExcluded;

                /*
                 *   Create a push traveler to coordinate the travel.  The
                 *   push traveler performs the normal travel, and also moves
                 *   the object being pushed.
                 */
                newTrav = pushTravelerClass.createInstance(
                    self, gActor.getPushTraveler(self));

                /* set the actor's special traveler to our push traveler */
                oldTrav = gActor.setSpecialTraveler(newTrav);

                /*
                 *   Add myself to the actor's look-around exclusion list -
                 *   this ensures that we won't be listed among the objects in
                 *   the new location when we show the description on arrival.
                 *   This is desirable because of how we describe the travel:
                 *   we first want to show the new location's description, then
                 *   describe pushing this object into the new location.  But
                 *   we actually have to move the object first, to make sure
                 *   any side effects of its presence in the new location are
                 *   taken into account when we describe the location - because
                 *   by the time we're in the room enough to have a look
                 *   around, the pushable will be in the room with us.  Even
                 *   so, we don't actually want to describe the pushable as in
                 *   the room at this stage, because from the player's
                 *   perspective we're really still in the process of pushing
                 *   it; describing it as already settled into the new location
                 *   sounds weird because it makes it seem like the object was
                 *   already in place when we arrived.  To avoid this
                 *   weirdness, suppress it from the new location's initial
                 *   description on arrival.
                 */
                wasExcluded = gActor.excludeFromLookAround(self);

                /* make sure we undo our global changes when we're done */
                try
                {
                    /*
                     *   Now that we've activated our special push traveler
                     *   for the actor, simply perform the ordinary travel
                     *   command.  The travel command we perform depends on
                     *   the kind of push-travel command we attempted, so
                     *   simply let the current action carry out the action.
                     */
                    gAction.performTravel();
                }
                finally
                {
                    /* restore the actor's old special traveler on the way out */
                    gActor.setSpecialTraveler(oldTrav);

                    /*
                     *   if we weren't in the actor's 'look around' exclusion
                     *   list before, remove us from the list
                     */
                    if (!wasExcluded)
                        gActor.unexcludeFromLookAround(self);
                }
            }
        }

        /*
         *   The class we create for our special push traveler - by default,
         *   this is PushTraveler, but we parameterize this via this property
         *   to allow special PushTraveler subclasses to be created; this
         *   could be useful, for example, to customize the traveler name
         *   messages.
         */
        pushTravelerClass = PushTraveler
    ;

    /*
     *   A special Traveler class for travel involving pushing an object from
     *   one room to another.  This class encapsulates the object being pushed
     *   and the actual Traveler performing the travel.
     *
     *   For the most part, we refer Traveler methods to the underlying
     *   Traveler.  We override a few methods to provide special handling.
     */
    class PushTraveler: object
        construct(obj, traveler)
        {
            /* remember the object being pushed and the real traveler */
            obj_ = obj;
            traveler_ = traveler;
        }

        /* the object being pushed */
        obj_ = nil

        /*
         *   the underlying Traveler - this is the real Traveler that will
         *   move to a new location
         */
        traveler_ = nil

        /*
         *   Travel to a new location.  We'll run the normal travel routine
         *   for the underlying real traveler; then, if we ended up in a new
         *   location, we'll move the object being pushed to the traveler's
         *   new location.
         */
        travelerTravelTo(dest, connector, backConnector)
        {
            local oldLoc;
            local origin;

            /* remember the traveler's origin, so we can tell if we moved */
            origin = traveler_.location;

            /* let the object being pushed describe the departure */
            obj_.beforeMovePushable(traveler_, connector, dest);

            /*
             *   *Tentatively* move the pushable to its new location.  Just do
             *   the basic move-into for this, since it's only tentative - this
             *   is just so that we get any side effects of its presence in the
             *   new location for the purposes of describing the new location.
             */
            oldLoc = location;
            obj_.baseMoveInto(dest);

            /*
             *   Call the traveler's normal travel routine, to perform the
             *   actual movement of the underlying traveler.
             */
            traveler_.travelerTravelTo(dest, connector, backConnector);

            /* undo the tentative move of the pushable */
            obj_.baseMoveInto(oldLoc);

            /*
             *   If we moved to a new location, we can now actually move the
             *   pushable object to the new location.
             */
            if (traveler_.location != origin)
                obj_.movePushable(traveler_, connector);
        }

        /*
         *   Perform local travel, between nested rooms within a top-level
         *   location.  By default, we simply don't allow pushing objects
         *   between nested rooms.
         *
         *   To allow pushing an object between nested rooms, override this in
         *   parallel with travelerTravelTo().  Note that you'll have to call
         *   travelerTravelWithin() on the underlying traveler (which will
         *   generally be the actor), and you'll probably want to set up a new
         *   set of notifiers parallel to beforeMovePushable() and
         *   movePushable().  You'll probably particularly need to customize
         *   the report in your parallel for movePushable() - the default ("you
         *   push x into the area") isn't very good when nested rooms are
         *   involved, and you'll probably want something more specific.
         */
        travelerTravelWithin(actor, dest)
        {
            reportFailure(&cannotPushObjectNestedMsg, obj_);
            exit;
        }

        /*
         *   Can we travel via the given connector?  We'll ask our underlying
         *   traveler first, and if that succeeds, we'll ask the object we're
         *   pushing.
         */
        canTravelVia(connector, dest)
        {
            /* ask the underlying traveler first, then our pushed object */
            return (traveler_.canTravelVia(connector, dest)
                    && obj_.canPushTravelVia(connector, dest));
        }

        /*
         *   Explain why the given travel is not possible.  If our underlying
         *   traveler raised the objection, let it explain; otherwise, let our
         *   pushed object explain.
         */
        explainNoTravelVia(connector, dest)
        {
            if (!traveler_.canTravelVia(connector, dest))
                traveler_.explainNoTravelVia(connector, dest);
            else
                obj_.explainNoPushTravelVia(connector, dest);
        }

        /* by default, send everything to the underlying Traveler */
        propNotDefined(prop, [args]) { return traveler_.(prop)(args...); }
    ;

    /*
     *   A PushTravelBarrier is a TravelConnector that allows regular travel,
     *   but not travel that involves pushing something.  By default, we block
     *   all push travel, but subclasses can customize this so that we block
     *   only specific objects.
     */
    class PushTravelBarrier: TravelBarrier
        /*
         *   Determine if the given pushed object is allowed to pass.  Returns
         *   true if so, nil if not.  By default, we'll return nil for every
         *   object; subclasses can override this to allow some objects to be
         *   pushed through the barrier but not others.
         */
        canPushedObjectPass(obj) { return nil; }

        /* explain why an object can't pass */
        explainTravelBarrier(traveler)
        {
            reportFailure(&cannotPushObjectThatWayMsg, traveler.obj_);
        }

        /*
         *   Determine if the given traveler can pass through this connector.
         *   If the traveler isn't a push traveler, we'll allow the travel;
         *   otherwise, we'll block the travel if our canPushedObjectPass
         *   routine says the object being pushed can pass.
         */
        canTravelerPass(traveler)
        {
            /* if it's not a push traveler, it can pass */
            if (!traveler.ofKind(PushTraveler))
                return true;

            /* it can pass if we can pass the object being pushed */
            return canPushedObjectPass(traveler.obj_);
        }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A basic location - this is the base class for locations that can
     *   contain actors.
     */
    class BasicLocation: Thing
        /*
         *   Get the nested room list grouper for an actor in the given
         *   posture directly in this room.  This is used when we're listing
         *   the actors within the nested room as
         *
         *   By default, we maintain a lookup table, and store one nested
         *   actor grouper object for each posture.  This makes it so that we
         *   show one group per posture in this room; for example, if we
         *   contain two sitting actors and three standing actors, we'll say
         *   something like "bill and bob are sitting on the stage, and jill,
         *   jane, and jack are standing on the stage."  This can be
         *   overridden if a different arrangement of groups is desired; for
         *   example, an override could simply return a single grouper to list
         *   everyone in the room together, regardless of posture.
         */
        listWithActorIn(posture)
        {
            /* if we don't have a lookup table for this yet, create one */
            if (listWithActorInTable == nil)
                listWithActorInTable = new LookupTable(5, 5);

            /* if this posture isn't in the table yet, create a grouper for it */
            if (listWithActorInTable[posture] == nil)
                listWithActorInTable[posture] =
                    new RoomActorGrouper(self, posture);

            /* return the grouper for this posture */
            return listWithActorInTable[posture];
        }

        /*
         *   our listWithActorIn table - this gets initialized to a
         *   LookupTable as soon as we need one (in listWithActorIn)
         */
        listWithActorInTable = nil

        /*
         *   Check the ambient illumination level in the room for the given
         *   actor's senses to determine if the actor would be able to see if
         *   the actor were in this room without any additional light sources.
         *   Returns true if the room is lit for the purposes of the actor's
         *   visual senses, nil if not.
         *
         *   Note that if the actor is already in this location, there's no
         *   need to run this test, since we could get the information from
         *   the actor directly.  The point of this test is to determine the
         *   light level in this location without the actor having to be
         *   present.
         */
        wouldBeLitFor(actor)
        {
            /*
             *   Check for a simple, common case before running the more
             *   expensive full what-if test.  Most rooms provide their own
             *   illumination in the actor's 'sight' sense; if this room does
             *   provide its own interior 'brightness' of at least 2, and the
             *   actor has 'sight' among its visual senses, then the room is
             *   lit.  Note that we must use transSensingOut() to determine
             *   how we transmit our own brightness to our interior, since
             *   that gives the transparency for looking from within this room
             *   at objects outside the room; our own intrinsic brightness is
             *   defined as the brightness of our exterior surface.
             */
            if (actor.sightlikeSenses.indexOf(sight) != nil
                && brightness > 1
                && transSensingOut(sight) == transparent)
                return true;

            /*
             *   We can't determine for sure that the location is lit with a
             *   simple test, so run a full what-if test, checking what
             *   ambient light level our "probe" object would see if it were
             *   moved to this location.  Return true if the ambient light
             *   level is higher than the "self-lit" value of 1, nil if not.
             */
            return (lightProbe.whatIf(
                {: senseAmbientMax(actor.sightlikeSenses) },
                &moveInto, self) > 1);
        }

        /*
         *   Show our room description: this is the interior description of
         *   the room, for use when the room is viewed by an actor within the
         *   room.  By default, we show our ordinary 'desc'.
         */
        roomDesc { desc; }

        /* as part of a room description, mention an actor in this room */
        roomActorHereDesc(actor) { gLibMessages.actorInRoom(actor, self); }

        /*
         *   Provide a default description for an actor in this location, as
         *   seen from a remote location (i.e., from a separate top-level room
         *   that's linked to our top-level room by a sense connector of some
         *   kind).  By default, we'll describe the actor as being in this
         *   nested room.
         */
        roomActorThereDesc(actor)
        {
            local pov = getPOV();
            local outer;

            /* get the outermost visible enclosing room */
            outer = getOutermostVisibleRoom(pov);

            /*
             *   If we found a room, and it's not us (i.e., we found something
             *   outside this room that can be seen from the current point of
             *   view), use the three-part description: actor in nested room
             *   (self) in outer room.  If we didn't find a room, or it's the
             *   same as us, all we can say is that the actor is in the nested
             *   room.
             */
            if (outer not in (nil, self))
            {
                /* use the three-part description: actor in self in outer */
                gLibMessages.actorInRemoteNestedRoom(actor, self, outer, pov);
            }
            else
            {
                /* we're as far out as we can see: just say actor is in self */
                gLibMessages.actorInRemoteRoom(actor, self, pov);
            }
        }

        /* show the status addendum for an actor in this location */
        roomActorStatus(actor) { gLibMessages.actorInRoomStatus(actor, self); }

        /* describe the actor's posture while in this location */
        roomActorPostureDesc(actor)
            { gLibMessages.actorInRoomPosture(actor, self); }

        /* acknowledge a posture change while in this location */
        roomOkayPostureChange(actor)
            { defaultReport(&roomOkayPostureChangeMsg, actor.posture, self); }

        /*
         *   describe the actor's posture as part of the EXAMINE description of
         *   the nested room
         */
        roomListActorPosture(actor)
            { gLibMessages.actorInRoom(actor, self); }

        /*
         *   Prefix and suffix messages for listing a group of actors
         *   nominally in this location.  'posture' is the posture of the
         *   actors.  'remote' is the outermost visible room containing the
         *   actors, but only if that room is remote from the point-of-view
         *   actor; if everything is local, this will be nil.  'lst' is the
         *   list of actors being listed.  By default, we'll just show the
         *   standard library messages.
         */
        actorInGroupPrefix(pov, posture, remote, lst)
        {
            if (remote == nil)
                gLibMessages.actorInGroupPrefix(posture, self, lst);
            else
                gLibMessages.actorInRemoteGroupPrefix(
                    pov, posture, self, remote, lst);
        }
        actorInGroupSuffix(pov, posture, remote, lst)
        {
            if (remote == nil)
                gLibMessages.actorInGroupSuffix(posture, self, lst);
            else
                gLibMessages.actorInRemoteGroupSuffix(
                    pov, posture, self, remote, lst);
        }

        /*
         *   Show a list of exits from this room as part of failed travel
         *   ("you can't go that way").
         */
        cannotGoShowExits(actor)
        {
            /* if we have an exit lister, ask it to show exits */
            if (gExitLister != nil)
                gExitLister.cannotGoShowExits(actor, self);
        }

        /* show the exit list in the status line */
        showStatuslineExits()
        {
            /* if we have a global exit lister, ask it to show the exits */
            if (gExitLister != nil)
                gExitLister.showStatuslineExits();
        }

        /*
         *   Get the estimated height, in lines of text, of the exits display's
         *   contribution to the status line.  This is used to calculate the
         *   extra height we need in the status line, if any, to display the
         *   exit list.  If we're not configured to display exits in the status
         *   line, this should return zero.
         */
        getStatuslineExitsHeight()
        {
            if (gExitLister != nil)
                return gExitLister.getStatuslineExitsHeight();
            else
                return 0;
        }

        /*
         *   Make the actor stand up from this location.  By default, we'll
         *   simply change the actor's posture to "standing," and show a
         *   default success report.
         *
         *   Subclasses might need to override this.  For example, a chair
         *   will set the actor's location to the room containing the chair
         *   when the actor stands up from the chair.
         */
        makeStandingUp()
        {
            /* simply set the actor's new posture */
            gActor.makePosture(standing);

            /* issue a default report of the change */
            defaultReport(&okayPostureChangeMsg, standing);
        }

        /*
         *   Default posture for an actor in the location.  This is the
         *   posture assumed by an actor when moving out of a nested room
         *   within this location.
         */
        defaultPosture = standing

        /* failure report we issue when we can't return to default posture */
        mustDefaultPostureProp = &mustBeStandingMsg

        /* run the appropriate implied command to achieve our default posture */
        tryMakingDefaultPosture()
        {
            return defaultPosture.tryMakingPosture(self);
        }

        /*
         *   Check this object as a staging location.  We're a valid location,
         *   so we allow this.
         */
        checkStagingLocation(dest)
        {
            /* we've valid, so we don't need to do anything */
        }

        /*
         *   Try moving the actor into this location.
         */
        checkMovingActorInto(allowImplicit)
        {
            /*
             *   If the actor isn't somewhere within us, we can't move the
             *   actor here implicitly - we have no generic way of causing
             *   implicit travel between top-level locations.  Note that some
             *   rooms might want to override this when travel between
             *   adjacent locations makes sense as an implicit action; we
             *   expect that such cases will be rare, so we don't attempt to
             *   generalize this possibility.
             */
            if (!gActor.isIn(self))
            {
                reportFailure(&cannotDoFromHereMsg);
                exit;
            }

            /*
             *   if the actor is already directly in me, simply check to make
             *   sure the actor is in the default posture for the room - if
             *   not, try running an appropriate implied command to change the
             *   posture
             */
            if (gActor.isDirectlyIn(self))
            {
                /* if the actor's already in the default posture, we're okay */
                if (gActor.posture == defaultPosture)
                    return nil;

                /* run the implied command to stand up (or whatever) */
                if (allowImplicit && tryMakingDefaultPosture())
                {
                    /* make sure we're in the proper posture now */
                    if (gActor.posture != defaultPosture)
                        exit;

                    /* note that we ran an implied command */
                    return true;
                }

                /* we couldn't get into the default posture - give up */
                reportFailure(mustDefaultPostureProp);
                exit;
            }

            /*
             *   The actor is within a nested room within me.  Find our
             *   immediate child containing the actor, and remove the actor
             *   from the child.
             */
            foreach (local cur in contents)
            {
                /* if this is the one containing the actor, remove the actor */
                if (gActor.isIn(cur))
                    return cur.checkActorOutOfNested(allowImplicit);
            }

            /* we didn't find the nested room with the actor, so give up */
            reportFailure(&cannotDoFromHereMsg);
            exit;
        }

        /*
         *   Check, using pre-condition rules, that the actor is ready to
         *   enter this room as a nested location.  By default, we do nothing,
         *   since we're not designed as a nested location.
         */
        checkActorReadyToEnterNestedRoom(allowImplicit)
        {
            return nil;
        }

        /*
         *   Check that the traveler is directly in the given room, using
         *   pre-condition rules.  'nested' is the nested location immediately
         *   within this room that contains the actor (directly or
         *   indirectly).
         */
        checkTravelerDirectlyInRoom(traveler, allowImplicit)
        {
            /* if the actor is already directly in this room, we're done */
            if (traveler.isDirectlyIn(self))
                return nil;

            /* try moving the actor here */
            return traveler.checkMovingTravelerInto(self, allowImplicit);
        }

        /*
         *   Check, using pre-condition rules, that the actor is removed from
         *   this nested location and moved to its exit destination.  By
         *   default, we're not a nested location, so there's nothing for us
         *   to do.
         */
        checkActorOutOfNested(allowImplicit)
        {
            /* we're not a nested location, so there's nothing for us to do */
            return nil;
        }

        /*
         *   Determine if the current gActor, who is directly in this location,
         *   is "travel ready."  This means that the actor is ready, as far as
         *   this location is concerned, to traverse the given connector.  By
         *   default, we consider an actor to be travel-ready if the actor is
         *   standing; this takes care of most nested room situations, such as
         *   chairs and beds, automatically.
         */
        isActorTravelReady(conn) { return gActor.posture == standing; }

        /*
         *   Run an implicit action, if possible, to make the current actor
         *   "travel ready."  This will be called if the actor is directly in
         *   this location and isActorTravelReady() returns nil.  By default,
         *   we try to make the actor stand up.  This should always be paired
         *   with isActorTravelReady - the condition that routine tests should
         *   be the condition this routine tries to bring into effect.  If no
         *   implicit action is possible, simply return nil.
         */
        tryMakingTravelReady(conn) { return tryImplicitAction(Stand); }

        /* the message explaining what we must do to be travel-ready */
        notTravelReadyMsg = &mustBeStandingMsg

        /*
         *   An actor is attempting to disembark this location.  By default,
         *   we'll simply turn this into an "exit" command.
         */
        disembarkRoom()
        {
            /* treat this as an 'exit' command */
            replaceAction(Out);
        }

        /*
         *   The destination for objects explicitly dropped by an actor within
         *   this room.  By default, we'll return self, because items dropped
         *   should simply go in the room itself.  Some types of rooms will
         *   want to override this; for example, a room that represents the
         *   middle of a tightrope would probably want to set the drop
         *   destination to the location below the tightrope.  Likewise,
         *   objects like chairs will usually prefer to have dropped items go
         *   into the enclosing room.
         */
        getDropDestination(objToDrop, path)
        {
            /* by default, objects dropped in this room end up in this room */
            return self;
        }

        /*
         *   The nominal drop destination - this is the location where objects
         *   are *reported* to go when dropped by an actor in this location.
         *   By default, we simply return 'self'.
         *
         *   The difference between the actual drop location and the nominal
         *   drop location is that the nominal drop location is used only for
         *   reporting messages, while the actual drop location is the
         *   location where objects are moved on 'drop' or equivalent actions.
         *   Rooms, for example, want to report that a dropped object lands on
         *   the floor (or the ground, or whatever), even though the room
         *   itself is the location where the object actually ends up.  We
         *   distinguish between the nominal and actual drop location to allow
         *   these distinctions in reported messages.
         */
        getNominalDropDestination() { return self; }

        /*
         *   The "nominal actor container" - this is the container which we'll
         *   say actors are in when we describe actors who are actually in
         *   this location.  By default, this simply returns self, but it's
         *   sometimes useful to describe actors as being in some object other
         *   than self.  The most common case is that normal top-level rooms
         *   usually want to describe actors as being "on the floor" or
         *   similar.
         */
        getNominalActorContainer(posture) { return self; }

        /*
         *   Get any extra items in scope for an actor in this location.
         *   These are items that are to be in scope even if they're not
         *   reachable through any of the normal sense paths (so they'll be in
         *   scope even in the dark, for example).
         *
         *   By default, this returns nothing.  Subclasses can override as
         *   necessary to include additional items in scope.  For example, a
         *   chair would probably want to include itself in scope, since the
         *   actor presumably knows he or she is sitting in a chair even if
         *   it's too dark to see the chair.
         */
        getExtraScopeItems(actor) { return []; }

        /*
         *   Receive notification that we're about to perform a command within
         *   this location.  This is called on the outermost room first, then
         *   on the nested rooms, from the outside in, until reaching the room
         *   directly containing the actor performing the command.
         */
        roomBeforeAction()
        {
        }

        /*
         *   Receive notification that we've just finished a command within
         *   this location.  This is called on the room immediately containing
         *   the actor performing the command, then on the room containing
         *   that room, and so on to the outermost room.
         */
        roomAfterAction()
        {
        }

        /*
         *   Get my notification list - this is a list of objects on which we
         *   must call beforeAction and afterAction when an action is
         *   performed within this room.
         *
         *   We'll also include any registered notification items for all of
         *   our containing rooms up to the outermost container.
         *
         *   The general notification mechanism always includes in the
         *   notification list all of the objects connected by containment to
         *   the actor, so objects that are in this room need not register for
         *   explicit notification.
         */
        getRoomNotifyList()
        {
            local lst;

            /* start with our explicitly registered list */
            lst = roomNotifyList;

            /* add notification items for our immediate locations  */
            forEachContainer(
                {cont: lst = lst.appendUnique(cont.getRoomNotifyList())});

            /* return the result */
            return lst;
        }

        /*
         *   Add an item to our registered notification list for actions in
         *   the room.
         *
         *   Items can be added here if they must be notified of actions
         *   performed by within the room even when the items aren't in the
         *   room at the time of the action.  All items connected by
         *   containment with the actor performing an action are automatically
         *   notified of the action; only items that must receive notification
         *   even when not connected by containment need to be registered
         *   here.
         */
        addRoomNotifyItem(obj)
        {
            roomNotifyList += obj;
        }

        /* remove an item from the registered notification list */
        removeRoomNotifyItem(obj)
        {
            roomNotifyList -= obj;
        }

        /* our list of registered notification items */
        roomNotifyList = []

        /*
         *   Get the room location.  Since we're capable of holding actors, we
         *   are our own room location.
         */
        roomLocation = (self)

        /*
         *   Receive notification that a traveler is arriving.  This is a
         *   convenience method that rooms can override to carry out side
         *   effects of arrival.  This is called just before the room's
         *   arrival message (usually the location description) is displayed,
         *   so the method can make any adjustments to the room's status or
         *   contents needed for the arrival.  By default, we do nothing.
         */
        enteringRoom(traveler) { }

        /*
         *   Receive notification that a traveler is leaving.  This is a
         *   convenience method that rooms can override to carry out side
         *   effects of departure.  This is called just after any departure
         *   message is displayed.  By default, we do nothing.
         */
        leavingRoom(traveler) { }

        /*
         *   Receive notification that a traveler is about to leave the room.
         *   'traveler' is the object actually traveling.  In most cases this
         *   is simply the actor; but when the actor is in a vehicle, this is
         *   the vehicle instead.
         *
         *   By default, we describe the traveler's departure if the traveler's
         *   destination is different from its present location.
         */
        travelerLeaving(traveler, dest, connector)
        {
            /* describe the departure */
            if (dest != traveler.location)
                traveler.describeDeparture(dest, connector);

            /* run the departure notification */
            leavingRoom(traveler);
        }

        /*
         *   Receive notification that a traveler is arriving in the room.
         *   'traveler' is the object actually traveling.  In most cases this
         *   is simply the actor; but when the actor is in a vehicle, this is
         *   the vehicle instead.
         *
         *   By default, we set each of the "motive" actors to its default
         *   posture, then describe the arrival.
         */
        travelerArriving(traveler, origin, connector, backConnector)
        {
            /*
             *   Set the self-motive actors into the proper default posture
             *   for the location.  We only do this for actors moving under
             *   their own power, since actors in vehicles will presumably
             *   just stay in the posture that's appropriate for the vehicle.
             */
            foreach (local actor in traveler.getTravelerMotiveActors)
            {
                /*
                 *   If the actor isn't in this posture already, set the actor
                 *   to the new posture.  Note that we set the new posture
                 *   directly, rather than via a nested command; we don't want
                 *   the travel to consist of a NORTH plus a SIT, but simply of
                 *   a NORTH.  Note that this could bypass side effects
                 *   normally associated with the SIT (or whatever), but we
                 *   assume that when a room with a specific posture is linked
                 *   directly from a separate location, the travel connector
                 *   linking up to the new room will take care of the necessary
                 *   side effects.
                 */
                if (actor.posture != defaultPosture)
                    actor.makePosture(defaultPosture);
            }

            /* run the arrival notification */
            enteringRoom(traveler);

            /* describe the arrival */
            traveler.describeArrival(origin, backConnector);
        }

        /*
         *   Receive notification of travel among nested rooms.  When an actor
         *   moves between two locations related directly by containing (such
         *   as from a chair to the room containing the chair, or vice versa),
         *   we first call this routine on the origin of the travel, then we
         *   move the actor, then we call this same routine on the destination
         *   of the travel.
         *
         *   This routine is used any time an actor is moved with
         *   travelWithin().  This is not used when an actor travels between
         *   locations related by a TravelConnector object rather than by
         *   direct containment.
         *
         *   We do nothing by default.  Locations can override this if they
         *   wish to perform any special handling during this type of travel.
         */
        actorTravelingWithin(origin, dest)
        {
        }

        /*
         *   Determine if the given actor has "intrinsic" knowledge of the
         *   destination of the given travel connector leading away from this
         *   location.  This knowledge is independent of any memory the actor
         *   has of actual travel through the connector in the course of the
         *   game, which we track separately via the TravelConnector's travel
         *   memory mechanism.
         *
         *   There are two main reasons an actor would have intrinsic
         *   knowledge of a connector's destination:
         *
         *   1. The actor is supposed to be familiar with the location and its
         *   surroundings, within the context of the game.  For example, if
         *   part of the game is the player character's own house, the PC
         *   would probably know where all of the connections among rooms go.
         *
         *   2. The destination location is plainly visible from this location
         *   or is clearly marked (such as with a sign).  For example, if the
         *   current location is an open field, a nearby hilltop to the east
         *   might be visible from here, so we could see from here where we'll
         *   end up by going east.  Alternatively, if we're in a lobby, and
         *   the passage to the west is marked with a sign reading "electrical
         *   room," an actor would have good reason to think an electrical
         *   room lies to the west.
         *
         *   We handle case (1) automatically through our actorIsFamiliar()
         *   method: if the actor is familiar with the location, we assume by
         *   default that the actor knows where all of the connectors from
         *   here go.  We don't have any default handling for case (2), so
         *   individual rooms (or subclasses) must override this method if
         *   they want to specify intrinsic knowledge for any of their
         *   outgoing connectors.
         */
        actorKnowsDestination(actor, conn)
        {
            /*
             *   if the actor is familiar with this location, then the actor
             *   by default knows where all of the outgoing connections go
             */
            if (actorIsFamiliar(actor))
                return true;

            /* there's no other way the actor would know the destination */
            return nil;
        }

        /*
         *   Is the actor familiar with this location?  In other words, is the
         *   actor supposed to know the location well at the start of the game?
         *
         *   This should return true if the actor is familiar with this
         *   location, nil if not.  By default, we return nil, since actors
         *   are not by default familiar with any locations.
         *
         *   The purpose of this routine is to determine if the actor is meant
         *   to know the location well, within the context of the game, even
         *   before the game starts.  For example, if an area in the game is
         *   an actor's own house, the actor would naturally be familiar,
         *   within the context of the game, with the locations making up the
         *   house.
         *
         *   Note that this routine doesn't need to "learn" based on the
         *   events of the game.  The familiarity here is meant only to model
         *   the actor's knowledge as of the start of the game.
         */
        actorIsFamiliar(actor) { return nil; }

        /*
         *   The default "you can't go that way" message for travel within this
         *   location in directions that don't allow travel.  This is shown
         *   whenever an actor tries to travel in one of the directions we have
         *   set to point to noTravel.  A room can override this to produce a
         *   different, customized message for unset travel directions - this
         *   is an easy way to change the cannot-travel message for several
         *   directions at once.
         *
         *   The handling depends on whether or not it's dark.  If it's dark,
         *   we don't want to reveal whether or not it's actually possible to
         *   perform the travel, since there's no light to see where the exits
         *   are.
         */
        cannotTravel()
        {
            /* check for darkness */
            if (!gActor.isLocationLit())
            {
                /* the actor is in the dark - use our dark travel message */
                cannotGoThatWayInDark();
            }
            else
            {
                /* use the standard "can't go that way" routine */
                cannotGoThatWay();
            }
        }

        /*
         *   Receive notification of travel from one dark location to another.
         *   This is called before the actor is moved from the source
         *   location, and can cancel the travel if desired by using 'exit' to
         *   terminate the command.
         *
         *   By default, we'll simply display the same handler we do when the
         *   player attempts travel in a direction with no travel possible in
         *   the dark (cannotGoThatWayInDark), and then use 'exit' to cancel
         *   the command.  This default behavior provides the player with no
         *   mapping information in the dark, since the same message is
         *   generated whether or not travel would be possible in a given
         *   direction were light present.
         */
        roomDarkTravel(actor)
        {
            /*
             *   show the same message we would show if we attempted travel in
             *   the dark in a direction with no exit
             */
            cannotGoThatWayInDark();

            /* terminate the command */
            exit;
        }

        /*
         *   Show the default "you can't go that way" message for this
         *   location.  By default, we show a generic message, but individual
         *   rooms might want to override this to provide a more specific
         *   description of why travel isn't allowed.
         */
        cannotGoThatWay()
        {
            /* "you can't go that way" */
            reportFailure(cannotGoThatWayMsg);

            /* show a list of exits, if appropriate */
            cannotGoShowExits(gActor);
        }

        /*
         *   The message to display when it's not possible to travel in a given
         *   direction from this room; this is either a single-quoted string or
         *   an actor action messages property (by default, it's the latter,
         *   giving a default library message).
         */
        cannotGoThatWayMsg = &cannotGoThatWayMsg

        /*
         *   Show a version of the "you can't go that way" message for travel
         *   while in the dark.  This is called when the actor is in the dark
         *   (i.e., there's no ambient light at the actor) and attempts to
         *   travel in a direction that doesn't allow travel.  By default, we
         *   show a generic "you can't see where you're going in the dark"
         *   message.
         *
         *   This routine is essentially a replacement for the
         *   cannotGoThatWay() routine that we use when the actor is in the
         *   dark.
         */
        cannotGoThatWayInDark()
        {
            /* "it's too dark; you can't see where you're going */
            reportFailure(&cannotGoThatWayInDarkMsg);
        }

        /*
         *   Get preconditions for travel for an actor in this location.  These
         *   preconditions should be applied by any command that will involve
         *   travel from this location.  By default, we impose no additional
         *   requirements.
         */
        roomTravelPreCond = []

        /*
         *   Get the effective location of an actor directly within me, for
         *   the purposes of a "follow" command.  To follow someone, we must
         *   have the same effective follow location that the target had when
         *   we last observed the target leaving.
         *
         *   For most rooms, this is simply the room itself.
         */
        effectiveFollowLocation = (self)

        /*
         *   Dispatch the room daemon.  This is a daemon routine invoked once
         *   per turn; we in turn invoke roomDaemon on the current player
         *   character's current location.
         */
        dispatchRoomDaemon()
        {
            /* call roomDaemon on the player character's location */
            if (gPlayerChar.location != nil)
                gPlayerChar.location.roomDaemon();
        }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Room: the basic class for top-level game locations (that is, game
     *   locations that aren't inside any other simulation objects, but are at
     *   the top level of the containment hierarchy).  This is the smallest
     *   unit of movement; we do not distinguish among locations within a
     *   room, even if a Room represents a physically large location.  If it
     *   is necessary to distinguish among different locations in a large
     *   physical room, simply divide the physical room into sections and
     *   represent each section with a separate Room object.
     *
     *   A Room is not necessarily indoors; it is simply a location where an
     *   actor can be located.  This peculiar usage of "room" to denote any
     *   atomic location, even outdoors, was adopted by the authors of the
     *   earliest adventure games, and has been the convention ever since.
     *
     *   A room's contents are the objects contained directly within the room.
     *   These include fixed features of the room as well as loose items in
     *   the room, which are effectively "on the floor" in the room.
     *
     *   The Room class implements the Travel Connector interface in such a
     *   way that travel from one room to another can be established simply by
     *   setting a direction property (north, south, etc) in the origin room
     *   to point to the destination room.  This type of travel link has no
     *   side effects and is unconditional.
     *
     *   A room is by default an indoor location; this means that it contains
     *   walls, floor, and ceiling.  An outdoor location should be based on
     *   OutdoorRoom rather than Room.
     */
    class Room: Fixture, BasicLocation, RoomAutoConnector
        /*
         *   Initialize
         */
        initializeThing()
        {
            /* inherit default handling */
            inherited();

            /*
             *   Add my room parts to my contents list.  Only include room
             *   parts that don't have explicit locations.
             */
            contents += roomParts.subset({x: x.location == nil});
        }

        /*
         *   we're a "top-level" location: we don't have any other object
         *   containing us, but we're nonetheless part of the game world, so
         *   we're at the top level of the containment tree
         */
        isTopLevel = true

        /*
         *   we generally do not want rooms to be included when a command
         *   refers to 'all'
         */
        hideFromAll(action) { return true; }

        /* don't consider myself a default for STAND ON, SIT ON, or LIE ON */
        hideFromDefault(action)
        {
            /* don't hide from STAND ON, SIT ON, LIE ON */
            if (action.ofKind(StandOnAction)
                || action.ofKind(SitOnAction)
                || action.ofKind(LieOnAction))
                return nil;

            /* don't hide from defaults for other actions, though */
            return inherited(action);
        }

        /*
         *   Most rooms provide their own implicit lighting.  We'll use
         *   'medium' lighting (level 3) by default, which provides enough
         *   light for all activities, but is reduced to dim light (level 2)
         *   when it goes through obscuring media or over distance.
         */
        brightness = 3

        /*
         *   Get my "destination name," as seen by the given actor from the
         *   given origin location.  This gives the name we can use to
         *   describe this location from the perspective of an actor in an
         *   adjoining location looking at a travel connector from that
         *   location to here.
         *
         *   By default, we simply return our destName property.  This default
         *   behavior can be overridden if it's necessary for a location to
         *   have different destination names in different adjoining
         *   locations, or when seen by different actors.
         *
         *   If this location's name cannot or should not be described from an
         *   adjoining location, this should simply return nil.
         */
        getDestName(actor, origin) { return destName; }

        /*
         *   Our destination name, if we have one.  By default, we make this
         *   nil, which means the room cannot be described as a destination of
         *   connectors from adjoining locations.
         */
        destName = nil

        /*
         *   My "atmosphere" list.  This can be set to an EventList object to
         *   provide atmosphere messages while the player character is within
         *   this room.  The default roomDaemon will show one message from this
         *   EventList (by calling the EventList's doScript() method) on each
         *   turn the player character is in this location.
         */
        atmosphereList = nil

        /*
         *   Room daemon - this is invoked on the player character's immediate
         *   location once per turn in a daemon.
         */
        roomDaemon()
        {
            /*
             *   if we have an atmosphere message list, display the next
             *   message
             */
            if (atmosphereList != nil)
            {
                /* show visual separation, then the current atmosphere message */
                "<.commandsep>";
                atmosphereList.doScript();
            }
        }

        /*
         *   The nominal drop destination - this is the location where we
         *   describe objects as being when they're actually directly within
         *   the room.
         *
         *   By default, we return the object representing the room's floor.
         *   If there's no floor, we simply return 'self'.
         */
        getNominalDropDestination()
        {
            local floor;

            /*
             *   if there's a floor, it's the nominal drop destination;
             *   otherwise, just indicate that our contents are in 'self'
             */
            return ((floor = roomFloor) != nil ? floor : self);
        }

        /*
         *   Since we could be our own nominal drop destination, we need a
         *   message to describe things being put here.
         */
        putDestMessage = &putDestRoom

        /*
         *   The nominal actor container.  By default, this is the room's
         *   nominal drop destination, which is usually the floor or
         *   equivalent.
         */
        getNominalActorContainer(posture) { return getNominalDropDestination(); }

        /*
         *   move something into a room is accomplished by putting the object
         *   on the floor
         */
        tryMovingObjInto(obj)
        {
            local floor;

            /* if we have a floor, put the object there */
            if ((floor = roomFloor) != nil)
                return tryImplicitAction(PutOn, obj, floor);
            else
                return nil;
        }

        /* explain that something must be in the room first */
        mustMoveObjInto(obj)
        {
            local floor;

            /* if we have a floor, say that the object has to go there */
            if ((floor = roomFloor) != nil)
                reportFailure(&mustBeInMsg, obj, floor);
            else
                inherited(obj);
        }

        /*
         *   Get the apparent location of one of our room parts.
         *
         *   In most cases, we use generic objects (defaultFloor,
         *   defaultNorthWall, etc.) for the room parts.  There's only one
         *   instance of each of these generic objects in the whole game -
         *   there's only one floor, one north wall, and so on - so these
         *   instances can't have specific locations the way normal objects do.
         *   Thus the need for this method: this tells us the *apparent*
         *   location of one of the room part objects as perceived from this
         *   room.
         *
         *   If the part isn't in this location, we'll return nil.
         */
        getRoomPartLocation(part)
        {
            local loc;

            /*
             *   if this part is in our part list, then its apparent location
             *   is 'self'
             */
            if (roomParts.indexOf(part) != nil)
                return self;

            /*
             *   if the room part has an explicit location itself, and that
             *   location is either 'self' or is in 'self', return the
             *   location
             */
            if (part != nil
                && (loc = part.location) != nil
                && (loc == self || loc.isIn(self)))
                return loc;

            /* we don't have the part */
            return nil;
        }

        /*
         *   Get the list of extra parts for the room.  An indoor location has
         *   walls, floor, and ceiling; these are all generic objects that are
         *   included for completeness only.
         *
         *   A room with special walls, floor, or ceiling should override this
         *   to eliminate the default objects from appearing in the room.
         *   Note that if the room has a floor, it should always be
         *   represented by an object of class Floor, and should always be
         *   part of this list.
         */
        roomParts = [defaultFloor, defaultCeiling,
                     defaultNorthWall, defaultSouthWall,
                     defaultEastWall, defaultWestWall]

        /*
         *   Get the room's floor.  This looks for an object of class Floor in
         *   the roomParts list; if there is no such object, we'll return nil,
         *   indicating that the room has no floor at all.
         */
        roomFloor = (roomParts.valWhich({x: x.ofKind(Floor)}))

        /*
         *   Get any extra items in scope in this location.  These are items
         *   that are to be in scope even if they're not reachable through any
         *   of the normal sense paths (so they'll be in scope even in the
         *   dark, for example).
         *
         *   By default, if we have a floor, and the actor is directly in this
         *   room, we return our floor: this is because an actor is presumed to
         *   be in physical contact with the floor whenever directly in the
         *   room, and thus the actor would be aware that there's a floor
         *   there, even if the actor can't see the floor.  In some rooms, it's
         *   desirable to have certain objects in scope because they're
         *   essential features of the room; for example, a location that is
         *   part of a stairway would probably have the stairs in scope by
         *   virtue of the actor standing on them.
         */
        getExtraScopeItems(actor)
        {
            local floor;

            /*
             *   if we have a floor, and the actor is in this room, explicitly
             *   make the floor in scope
             */
            if ((floor = roomFloor) != nil && actor.isDirectlyIn(self))
                return [floor];
            else
                return [];
        }

        /*
         *   When we're in the room, treat EXAMINE <ROOM> the same as LOOK
         *   AROUND.  (This will only work if the room is given vocabulary
         *   like a normal object.)
         */
        dobjFor(Examine)
        {
            verify()
            {
                /*
                 *   When we're in the room, downgrade the likelihood a bit, as
                 *   we'd rather inspect an object within the room if there's
                 *   something with the same name.  When we're *not* in the
                 *   room - meaning this is a remote room that's visible from
                 *   the player's location - downgrade the likelihood even
                 *   more, since we're more likely to want to examine the room
                 *   we're actually in than a remote room with the same name.
                 */
                if (gActor.isIn(self))
                    logicalRank(80, 'x room');
                else
                    logicalRank(70, 'x room');
            }

            action()
            {
                /*
                 *   if the looker is within the room, replace EXAMINE <self>
                 *   with LOOK AROUND; otherwise, use the normal description
                 */
                if (gActor.isIn(self) && gActor.canSee(self))
                    gActor.lookAround(LookRoomDesc | LookListSpecials
                                      | LookListPortables);
                else
                    inherited();
            }
        }

        /* treat LOOK IN <room> as EXAMINE <room> */
        dobjFor(LookIn) remapTo(Examine, self)

        /* LOOK UNDER and BEHIND are illogical */
        dobjFor(LookUnder) { verify { illogical(&cannotLookUnderMsg); } }
        dobjFor(LookBehind) { verify { illogical(&cannotLookBehindMsg); } }

        /* treat SMELL/LISTEN TO <room> as just SMELL/LISTEN */
        dobjFor(Smell) remapTo(SmellImplicit)
        dobjFor(ListenTo) remapTo(ListenImplicit)

        /* map STAND/SIT/LIE ON <room> to my default floor */
        dobjFor(StandOn) maybeRemapTo(roomFloor != nil, StandOn, roomFloor)
        dobjFor(SitOn) maybeRemapTo(roomFloor != nil, SitOn, roomFloor)
        dobjFor(LieOn) maybeRemapTo(roomFloor != nil, LieOn, roomFloor)

        /*
         *   treat an explicit GET OUT OF <ROOM> as OUT if there's an apparent
         *   destination for OUT; otherwise treat it as "vague travel," which
         *   simply tells the player that they need to specify a direction
         */
        dobjFor(GetOutOf)
        {
            remap()
            {
                /* remap only if this isn't an implied action */
                if (gAction.parentAction == nil)
                {
                    /*
                     *   if we have an apparent Out connection, go there;
                     *   otherwise it's not obvious where we're meant to go
                     */
                    if (out != nil && out.isConnectorApparent(self, gActor))
                        return [OutAction];
                    else
                        return [VagueTravelAction];
                }

                /* don't remap here - use the standard handling */
                return inherited();
            }
        }

        /*
         *   for BOARD and ENTER, there are three possibilities:
         *
         *   - we're already directly in this room, in which case it's
         *   illogical to travel here again
         *
         *   - we're in a nested room within this room, in which case ENTER
         *   <self> is the same as GET OUT OF <outermost nested room within
         *   self>
         *
         *   - we're in a separate top-level room that's connected by a sense
         *   connector, in which case ENTER <self> should be handled as TRAVEL
         *   VIA <connector from actor's current location to self>
         */
        dobjFor(Board) asDobjFor(Enter)
        dobjFor(Enter)
        {
            verify()
            {
                /*
                 *   if we're already here, entering the same location is
                 *   redundant; if we're not in a nested room, we need a travel
                 *   connector to get there from here
                 */
                if (gActor.isDirectlyIn(self))
                    illogicalAlready(&alreadyInLocMsg);
                else if (!gActor.isIn(self)
                         && gActor.location.getConnectorTo(gActor, self) == nil)
                    illogicalNow(&whereToGoMsg);
            }
            preCond()
            {
                /*
                 *   if we're in a different top-level room, and there's a
                 *   travel connector, we'll simply travel via the connector,
                 *   so we don't need to impose any pre-conditions of our own
                 */
                if (!gActor.isIn(self)
                    && gActor.location.getConnectorTo(gActor, self) != nil)
                    return [];

                /*
                 *   if we're in a nested room within this object, we'll
                 *   replace the action with "get out of <outermost nested
                 *   room>", so there's no need for any extra preconditions
                 *   here
                 */
                if (gActor.isIn(self) && !gActor.isDirectlyIn(self))
                    return [];

                /* otherwise, use the default conditions */
                return inherited();
            }
            action()
            {
                /*
                 *   if we're in a nested room, get out; otherwise travel via a
                 *   suitable travel connector
                 */
                if (gActor.isIn(self) && !gActor.isDirectlyIn(self))
                {
                    /*
                     *   get out of the *outermost* nested room - that is, our
                     *   direct child that contains the actor
                     */
                    local chi = contents.valWhich({x: gActor.isIn(x)});

                    /* if we found it, get out of it */
                    if (chi != nil)
                        replaceAction(GetOutOf, chi);
                }
                else
                {
                    /* get the connector from here to there */
                    local conn = gActor.location.getConnectorTo(gActor, self);

                    /* if we found it, go that way */
                    if (conn != nil)
                        replaceAction(TravelVia, conn);
                }

                /*
                 *   if we didn't replace the action yet, we can't figure out
                 *   how to get here from the actor's current location
                 */
                reportFailure(&whereToGoMsg);
            }
        }
    ;

    /*
     *   A dark room, which provides no light of its own
     */
    class DarkRoom: Room
        /*
         *   turn off the lights
         */
        brightness = 0
    ;

    /*
     *   An outdoor location.  This differs from an indoor location in that it
     *   has ground and sky rather than floor and ceiling, and has no walls.
     */
    class OutdoorRoom: Room
        /* an outdoor room has ground and sky, but no walls */
        roomParts = [defaultGround, defaultSky]
    ;

    /*
     *   A shipboard room.  This is a simple mix-in class: it can be used
     *   along with any type of Room to indicate that this room is aboard a
     *   ship.  When a room is aboard a ship, the shipboard travel directions
     *   (port, starboard, fore, aft) are allowed; these directions normally
     *   make no sense.
     *
     *   This is a mix-in class rather than a Room subclass to allow it to be
     *   used in conjunction with any other Room subclass.  To make a room
     *   shipboard, simply declare your room like this:
     *
     *   mainDeck: Shipboard, Room // etc
     */
    class Shipboard: object
        /* mark the location as being aboard ship */
        isShipboard = true
    ;

    /*
     *   For convenience, we define ShipboardRoom as a shipboard version of the
     *   basic Room type.
     */
    class ShipboardRoom: Shipboard, Room
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Make a room "floorless."  This is a mix-in class that you can include
     *   in a superclass list ahead of Room or any of its subclasses to create
     *   a room where support is provided by some means other than standing on
     *   a surface, or where there's simply no support.  Examples: hanging on a
     *   rope over a chasm; climbing a ladder; in free-fall after jumping out
     *   of a plane; levitating in mid-air.
     *
     *   There are two main special features of a floorless room.  First, and
     *   most obviously, there's no "floor" or "ground" object among the room
     *   parts.  We accomplish this by simply subtracting out any object of
     *   class Floor from the room parts list inherited from the combined base
     *   room class.
     *
     *   Second, there's no place to put anything down, so objects dropped here
     *   either disappear from the game or are transported to another location
     *   (the room at the bottom of the chasm, for example).
     */
    class Floorless: object
        /*
         *   Omit the default floor/ground objects from the room parts list.
         *   Room classes generally have static room parts lists, so calculate
         *   this once per instance and store the results.
         *
         *   NOTE - if you combine Floorless with a base Room class that has a
         *   dynamic room parts list, you'll need to override this to calculate
         *   the subset dynamically on each invocation.
         */
        roomParts = perInstance(inherited().subset({x: !x.ofKind(Floor)}))

        /*
         *   The room below, if any - this is where objects dropped here will
         *   actually end up.  By default, this is nil, which means that
         *   objects dropped here simply disappear from the game.  If there's a
         *   "bottom of chasm" location where dropped objects should land,
         *   provide it here.
         */
        bottomRoom = nil

        /* receive a dropped object */
        receiveDrop(obj, desc)
        {
            /*
             *   move the dropped object to the room at the bottom of whatever
             *   it is we're suspended over; if there is no bottom room, we'll
             *   simply remove the dropped object from the game
             */
            obj.moveInto(bottomRoom);

            /*
             *   Say that the object drops out of sight below.  Build this by
             *   combining the generic report prefix from the drop descriptor
             *   with our generic suffix, which describes the object as
             *   vanishing below.
             */
            mainReport(desc.getReportPrefix(obj, self)
                       + gActor.getActionMessageObj().floorlessDropMsg(obj));
        }
    ;

    /*
     *   For convenience, provide a combination of Floorless with the ordinary
     *   Room.
     */
    class FloorlessRoom: Floorless, Room;

    /* ------------------------------------------------------------------------ */
    /*
     *   Room Part - base class for "parts" of rooms, such as floors and walls.
     *   Room parts are unusual in a couple of ways.
     *
     *   First, room parts are frequently re-used widely throughout a game.  We
     *   define a single instance of each of several parts that are found in
     *   typical rooms, and then re-use those instances in all rooms with those
     *   parts.  For example, we define one "default floor" object, and then
     *   use that object in most or all rooms that have floors.  We do this for
     *   efficiency, to avoid creating hundreds of essentially identical copies
     *   of the common parts.
     *
     *   Second, because room parts are designed to be re-used, things that are
     *   in or on the parts are actually represented in the containment model
     *   as being directly in their containing rooms.  For example, an object
     *   that is said to be "on the floor" actually has its 'location' property
     *   set to its immediate room container, and the 'contents' list that
     *   contains the object is that of the room, not of the floor object.  We
     *   must therefore override some of the normal handling for object
     *   locations within room parts, in order to make it appear (for the
     *   purposes of command input and descriptive messages) that the things
     *   are in/on their room parts, even though they're not really represented
     *   that way in the containment model.
     */
    class RoomPart: Fixture
        /*
         *   When we explicitly examine a RoomPart, list any object that's
         *   nominally contained in the room part, as long it doesn't have a
         *   special description for the purposes of the room part.  (If it
         *   does have a special description, then examining the room part will
         *   automatically display that special desc, so we don't want to
         *   include the object in a separate list of miscellaneous contents of
         *   the room part.)
         */
        isObjListedInRoomPart(obj)
        {
             /*
              *   list the object *unless* it has a special description for the
              *   purposes of examining this room part
              */
            return !obj.useSpecialDescInRoomPart(self);
        }

        /*
         *   Add this room part to the given room.
         *
         *   Room parts don't have normal "location" properties.  Instead, a
         *   room part explicitly appears in the "roomParts" list of each room
         *   that contains it.  For the most part, room parts are static -
         *   they're initialized in the room definitions and never changed.
         *   However, if you need to dynamically add a room part to a room
         *   during the game, you can do so using this method.
         */
        moveIntoAdd(room)
        {
            /* add me to the room's 'roomParts' and 'contents' lists */
            room.roomParts += self;
            room.contents += self;
        }

        /*
         *   Remove this room part from the given room.  This can be used if
         *   it's necessary to remove the room part dynamically from a room.
         */
        moveOutOf(room)
        {
            /* remove me from the room's 'roomParts' and 'contents' lists */
            room.roomParts -= self;
            room.contents -= self;
        }

        /*
         *   Don't include room parts in 'all'.  Room parts are so ubiquitous
         *   that we never want to assume that they're involved in a command
         *   except when it is specifically so stated.
         */
        hideFromAll(action) { return true; }

        /* do allow use as a default, though */
        hideFromDefault(action) { return nil; }

        /*
         *   When multiple room parts show up in a resolve list, and some of
         *   the parts are local to the actor's immediate location and others
         *   aren't, keep only the local ones.  This helps avoid pointless
         *   ambiguity in cases where two (or more) top-level locations are
         *   linked with a sense connector, and one or the other location has
         *   custom room part objects.
         */
        filterResolveList(lst, action, whichObj, np, requiredNum)
        {
            /* if a definite number of objects is required, check ambiguity */
            if (requiredNum != nil)
            {
                /* get the subset that's just RoomParts */
                local partLst = lst.subset({x: x.obj_.ofKind(RoomPart)});

                /*
                 *   get the *remote* subset - this is the subset that's not in
                 *   the outermost room of the target actor
                 */
                local outer = action.actor_.getOutermostRoom();
                local remoteLst = partLst.subset({x: !x.obj_.isIn(outer)});

                /*
                 *   If all of the objects are remote, or all of them are
                 *   local, we can't narrow things down on this basis; but if
                 *   we found some remote and some local, eliminate the remote
                 *   items, since we want to favor the local ones
                 */
                if (remoteLst.length() not in (0, partLst.length()))
                    lst -= remoteLst;
            }

            /* now do any inherited work, and return the result */
            return inherited(lst, action, whichObj, np, requiredNum);
        }

        /*
         *   Since room parts are generally things like walls and floors that
         *   enclose the entire room, they're typically visually large, and
         *   tend to have fairly large-scale details (such as doors and
         *   windows).  So, by default we set the sightSize to 'large' so that
         *   the details are visible at a distance.
         */
        sightSize = large

        /*
         *   as with decorations, downgrade the likelihood for Examine, as the
         *   standard walls, floors, etc. are pretty much background noise
         *   that are just here in case someone wants to refer to them
         *   explicitly
         */
        dobjFor(Examine)
        {
            verify()
            {
                inherited();
                logicalRank(70, 'x decoration');
            }
        }

        /* describe the status - shows the things that are in/on the part */
        examineStatus()
        {
            /* show the contents of the room part */
            examinePartContents(&descContentsLister);
        }

        /* show our contents */
        examinePartContents(listerProp)
        {
            /*
             *   Get my location, as perceived by the actor - this is the room
             *   that contains this part.  If I don't have a location as
             *   perceived by the actor, then we can't show any contents.
             */
            local loc = gActor.location.getRoomPartLocation(self);
            if (loc == nil)
                return;

            /*
             *   create a copy of the lister customized for this part, if we
             *   haven't already done so
             */
            if (self.(listerProp).part_ == nil)
            {
                self.(listerProp) = self.(listerProp).createClone();
                self.(listerProp).part_ = self;
            }

            /* show the contents of the containing location */
            self.(listerProp).showList(gActor, self, loc.contents, 0, 0,
                                       gActor.visibleInfoTable(), nil);
        }

        /*
         *   show our special contents - this shows objects with special
         *   descriptions that are specifically in this room part
         */
        examineSpecialContents()
        {
            local infoTab;
            local lst;

            /* get the actor's list of visible items */
            infoTab = gActor.visibleInfoTable();

            /*
             *   get the list of special description items, using only the
             *   subset that uses special descriptions in this room part
             */
            lst = specialDescList(infoTab,
                                  {obj: obj.useSpecialDescInRoomPart(self)});

            /* show the list */
            specialContentsLister.showList(gActor, nil, lst, 0, 0, infoTab, nil);
        }

        /*
         *   Get the destination for a thrown object that hits me.  Since we
         *   don't have a real location, we must ask the actor for our room
         *   part location, and then use its hit-and-fall destination.
         */
        getHitFallDestination(thrownObj, path)
        {
            local loc;
            local dest;

            /*
             *   if we have an explicit location, start with it; otherwise, ask
             *   the actor's location to find us; if we can't even find
             *   ourselves there, just use the actor's current location
             */
            if ((loc = location) == nil
                && (loc = gActor.location.getRoomPartLocation(self)) == nil)
                loc = gActor.location;

            /* use the location's drop destination for thrown objects */
            dest = loc.getDropDestination(thrownObj, path);

            /* give the destination a chance to make adjustments */
            return dest.adjustThrowDestination(thrownObj, path);
        }

        /* consider me to be in any room of which I'm a part */
        isIn(loc)
        {
            local rpl;

            /*
             *   get the room-part location of this room part, from the
             *   perspective of the prospective location we're asking about
             */
            if (loc != nil && (rpl = loc.getRoomPartLocation(self)) != nil)
            {
                /*
                 *   We indeed have a room part location in the given
                 *   location, so we're a part of the overall location.  We
                 *   might be directly in the location (i.e., 'rpl' could
                 *   equal 'loc'), or we might be somewhere relative to 'loc'
                 *   (for example, we could be within a nested room within
                 *   'loc', in which case 'rpl' is a nested room unequal to
                 *   'loc' but is within 'loc').  So, if 'rpl' equals 'loc' or
                 *   is contained in 'loc', I'm within 'loc', because I'm
                 *   within 'rpl'.
                 */
                if (rpl == loc || rpl.isIn(loc))
                    return true;
            }

            /*
             *   we don't appear to be in 'loc' on the basis of our special
             *   room-part location; fall back on the inherited handling
             */
            return inherited(loc);
        }

        /* our contents listers */
        contentsLister = roomPartContentsLister
        descContentsLister = roomPartDescContentsLister
        lookInLister = roomPartLookInLister
        specialContentsLister = specialDescLister

        /* look in/on: show our contents */
        dobjFor(LookIn)
        {
            verify() { }
            action()
            {
                /* show my contents */
                examinePartContents(&lookInLister);

                /* show my special contents */
                examineSpecialContents();
            }
        }

        /* we can't look behind/through/under a room part by default */
        nothingUnderMsg = &cannotLookUnderMsg
        nothingBehindMsg = &cannotLookBehindMsg
        nothingThroughMsg = &cannotLookThroughMsg

        /*
         *   initialization - add myself to my location's roomPart list if I
         *   have an explicit location
         */
        initializeThing()
        {
            /* do the normal work first */
            inherited();

            /*
             *   if I have an explicit location, and I'm not in my location's
             *   roomPart list, add myself to the list
             */
            if (location != nil && location.roomParts.indexOf(self) == nil)
                location.roomParts += self;
        }
    ;

    /*
     *   A floor for a nested room.  This should be placed directly within a
     *   nested room object if the nested room is to be described as having a
     *   floor separate from the nested room itself.  We simply remap any
     *   commands relating to using the floor as a surface (Put On, Throw At,
     *   Sit On, Lie On, Stand On) to the enclosing nested room.
     */
    class NestedRoomFloor: Fixture
        iobjFor(PutOn) remapTo(PutOn, DirectObject, location)
        iobjFor(ThrowAt) remapTo(ThrowAt, DirectObject, location)
        dobjFor(SitOn) remapTo(SitOn, location)
        dobjFor(LieOn) remapTo(LieOn, location)
        dobjFor(StandOn) remapTo(StandOn, location)
    ;

    /*
     *   Base class for the default floor and the default ground of a top-level
     *   room.  The floor and ground are where things usually go when dropped,
     *   and they're the locations where actors within a room are normally
     *   standing.
     */
    class Floor: RoomPart
        /* specifically allow me as a default for STAND ON, SIT ON, and LIE ON */
        hideFromDefault(action)
        {
            /* don't hide from STAND ON, SIT ON, LIE ON */
            if (action.ofKind(StandOnAction)
                || action.ofKind(SitOnAction)
                || action.ofKind(LieOnAction))
                return nil;

            /* for other actions, use the standard handling */
            return inherited(action);
        }

        /*
         *   When explicitly examining a Floor object, list any objects that
         *   are listed in the normal room description (as in LOOK AROUND).  By
         *   default, the floor is the nominal container for anything directly
         *   in the room, so we'll normally want LOOK AROUND and LOOK AT FLOOR
         *   to produce the same list of objects.
         */
        isObjListedInRoomPart(obj)
        {
             /* list the object if it's listed in a normal LOOK AROUND */
            return obj.isListed;
        }

        /*
         *   'put x on floor' equals 'drop x'.  Add a precondition that the
         *   drop destination is the main room, since otherwise we could have
         *   strange results if we dropped something inside a nested room.
         */
        iobjFor(PutOn)
        {
            preCond()
            {
                /*
                 *   require that this floor object itself is reachable, and
                 *   that the drop destination for the direct object is an
                 *   outermost room
                 */
                return [touchObj, new ObjectPreCondition(
                    gDobj, dropDestinationIsOuterRoom)];
            }
            verify() { }
            action() { replaceAction(Drop, gDobj); }
        }

        /*
         *   The message we use to describe this object prepositionally, as the
         *   destination of a throw or drop.  This should be a gLibMessages
         *   property with the appropriate prepositional phrase.  We use a
         *   custom message specific to floor-like objects.
         */
        putDestMessage = &putDestFloor

        /* 'throw x at floor' */
        iobjFor(ThrowAt)
        {
            check()
            {
                /*
                 *   If I'm reachable, suggest just putting it down instead.
                 *   We only make the suggestion, rather than automatically
                 *   treating the command as DROP, because a player explicitly
                 *   typing THROW <obj> AT FLOOR is probably attempting to
                 *   express something more violent than merely putting the
                 *   object down; the player probably is thinking in terms of
                 *   breaking the object (or the floor).
                 */
                if (canBeTouchedBy(gActor))
                {
                    mainReport(&shouldNotThrowAtFloorMsg);
                    exit;
                }
            }
        }

        /* is the given actor already on the floor? */
        isActorOnFloor(actor)
        {
            /*
             *   the actor is on the floor if the actor is directly in the
             *   floor's room-part-location for the actor's location
             */
            return actor.isDirectlyIn(actor.location.getRoomPartLocation(self));
        }

        /* verify sitting/standing/lying on the floor */
        verifyEntry(newPosture, alreadyMsg)
        {
            /*
             *   If we're already in my location, and we're in the desired
             *   posture, this command is illogical because we're already
             *   where they want us to end up.  Otherwise, it's logical to
             *   stand/sit/lie on the floor, but rank it low since we don't
             *   want the floor to interfere with selecting a default if
             *   there's anything around that's actually like a chair.
             *
             *   If it's logical, note that we've verified okay for the
             *   action.  On a future pass, we might have enforced a
             *   precondition that moved us here, at which point our work will
             *   be done - but we don't want to complain in that case, since
             *   it was logical from the player's perspective to carry out the
             *   command even though we have nothing left to do.
             */
            if (gActor.posture == newPosture
                && isActorOnFloor(gActor)
                && gAction.verifiedOkay.indexOf(self) == nil)
            {
                /* we're already on the floor in the desired posture */
                illogicalNow(alreadyMsg);
            }
            else
            {
                /*
                 *   it's logical, but rank it low in case there's something
                 *   more special we can stand/sit/lie on
                 */
                logicalRank(50, 'on floor');

                /*
                 *   note that we've verified okay, so we don't complain on a
                 *   future pass if we discover that a precondition has
                 *   brought us into compliance with the request prematurely
                 */
                gAction.verifiedOkay += self;
            }
        }

        /* perform sitting/standing/lying on the floor */
        performEntry(newPosture)
        {
            /*
             *   bring the new posture into effect; there's no need for
             *   actually moving the actor, since the preconditions will have
             *   moved us to our main enclosing room already
             */
            gActor.makePosture(newPosture);

            /* report success */
            defaultReport(&roomOkayPostureChangeMsg, newPosture, self);
        }

        /* 'stand on floor' causes actor to stand in the containing room */
        dobjFor(StandOn)
        {
            preCond = [touchObj,
                       new ObjectPreCondition(gActor.location.getOutermostRoom(),
                                              actorDirectlyInRoom)]
            verify() { verifyEntry(standing, &alreadyStandingOnMsg); }
            action() { performEntry(standing); }
        }

        /* 'sit on floor' causes the actor to sit in the containing room */
        dobjFor(SitOn)
        {
            preCond = [touchObj,
                       new ObjectPreCondition(gActor.location.getOutermostRoom(),
                                              actorDirectlyInRoom)]
            verify() { verifyEntry(sitting, &alreadySittingOnMsg); }
            action() { performEntry(sitting); }
        }

        /* 'lie on floor' causes the actor to lie down in the room */
        dobjFor(LieOn)
        {
            preCond = [touchObj,
                       new ObjectPreCondition(gActor.location.getOutermostRoom(),
                                              actorDirectlyInRoom)]
            verify() { verifyEntry(lying, &alreadyLyingOnMsg); }
            action() { performEntry(lying); }
        }

        /*
         *   Mention that an actor is here, as part of a room description.
         *   When the actor is standing, just say that the actor is here, since
         *   it's overstating the obvious to say that the actor is standing on
         *   the floor.  For other postures, do mention the floor.
         */
        roomActorHereDesc(actor)
        {
            /*
             *   if we're standing, just say that the actor is "here";
             *   otherwise, say that the actor is sitting/lying/etc on self
             */
            if (actor.posture == standing)
                gLibMessages.roomActorHereDesc(actor);
            else
                gLibMessages.actorInRoom(actor, self);
        }

        /*
         *   Mention that an actor is here, as part of a room description.
         *   Since a floor is a trivial part of its enclosing room, there's no
         *   point in mentioning that we're on the floor, as that's stating the
         *   obvious; instead, simply describe the actor as being in the
         *   actor's actual enclosing room.
         */
        roomActorThereDesc(actor) { actor.location.roomActorThereDesc(actor); }

        /*
         *   Show our room name status for an actor on the floor.  Since
         *   standing on the floor is the trivial default for any room, we
         *   won't bother mentioning it.  Other postures we'll mention the same
         *   way we would for any nested room.
         */
        roomActorStatus(actor)
        {
            if (actor.posture != standing)
                gLibMessages.actorInRoomStatus(actor, self);
        }

        /*
         *   Show the actor's posture here.  When we're standing on the floor,
         *   don't mention the posture, as this is too trivial a condition to
         *   state.  Otherwise, mention it as normal for a nested room.
         */
        roomActorPostureDesc(actor)
        {
            if (actor.posture != standing)
                gLibMessages.actorInRoomPosture(actor, self);
        }

        /*
         *   Generate an acknowledgment for a posture change here.  If the
         *   actor is standing, just say "okay, you're now standing" without
         *   mentioning the floor, since standing on the floor is the trivial
         *   default.  For other postures, say that we're sitting/lying/etc on
         *   the floor.
         */
        roomOkayPostureChange(actor)
        {
            if (actor.posture == standing)
                defaultReport(&okayPostureChangeMsg, standing);
            else
                defaultReport(&roomOkayPostureChangeMsg, actor.posture, self);
        }

        /*
         *   mention the actor as part of the EXAMINE description of a nested
         *   room containing the actor
         */
        roomListActorPosture(actor)
        {
            /*
             *   Since standing is the default posture for an actor, and since
             *   the floor (or equivalent) is the default place to be standing,
             *   don't bother mentioning actors standing on a floor.
             *   Otherwise, mention that the actor is sitting/lying/etc here.
             */
            if (actor.posture != standing)
                gLibMessages.actorInRoom(actor, self);
        }

        /*
         *   Prefix and suffix messages for listing a group of actors
         *   nominally on the this floor.  Actors are said to be on the floor
         *   when they're really in the location containing the floor.
         *
         *   If we're talking about a remote location, simply describe it as
         *   the location rather than mentioning the floor, since the floor is
         *   a trivial part of the remote location not worth mentioning.
         *
         *   If we're local, and we're standing, we'll simply say that we're
         *   "standing here"; again, saying that we're standing on the floor
         *   is stating the obvious.  If we're not standing, we will mention
         *   that we're on the floor.
         */
        actorInGroupPrefix(pov, posture, remote, lst)
        {
            if (remote != nil)
                gLibMessages.actorThereGroupPrefix(pov, posture, remote, lst);
            else if (posture == standing)
                gLibMessages.actorHereGroupPrefix(posture, lst);
            else
                gLibMessages.actorInGroupPrefix(posture, self, lst);
        }
        actorInGroupSuffix(pov, posture, remote, lst)
        {
            if (remote != nil)
                gLibMessages.actorThereGroupSuffix(pov, posture, remote, lst);
            else if (posture == standing)
                gLibMessages.actorHereGroupSuffix(posture, lst);
            else
                gLibMessages.actorInGroupSuffix(posture, self, lst);
        }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Define the default room parts.
     */

    /*
     *   the default floor, for indoor locations
     */
    defaultFloor: Floor
    ;

    /*
     *   the default ceiling, for indoor locations
     */
    defaultCeiling: RoomPart
    ;

    /*
     *   The default walls, for indoor locations.  We provide a north, south,
     *   east, and west wall in each indoor location by default.
     */
    class DefaultWall: RoomPart
    ;

    defaultNorthWall: DefaultWall
    ;

    defaultSouthWall: DefaultWall
    ;

    defaultEastWall: DefaultWall
    ;

    defaultWestWall: DefaultWall
    ;

    /*
     *   the default sky, for outdoor locations
     */
    defaultSky: Distant, RoomPart
    ;

    /*
     *   The default ground, for outdoor locations.
     */
    defaultGround: Floor
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A "room part item" is an object that's specially described as being
     *   part of, or attached to, a RoomPart (a wall, ceiling, floor, or the
     *   like).  This is a mix-in class that can be combined with any ordinary
     *   object class (but usually with something non-portable, such as a
     *   Fixture or Immovable).  The effect of adding RoomPartItem to an
     *   object's superclasses is that a command like EXAMINE EAST WALL (or
     *   whichever room part the object is associated with) will display the
     *   object's specialDesc, but a simple LOOK will not.  This class is
     *   sometimes useful for things like doors, windows, ceiling fans, and
     *   other things attached to the room.
     *
     *   Note that this is a mix-in class, so you should always combine it with
     *   a regular Thing-based class.
     *
     *   When using this class, you should define two properties in the object:
     *   specialNominalRoomPartLocation, which you should set to the RoomPart
     *   (such as a wall) where the object should be described; and
     *   specialDesc, which is the description to show when the room part is
     *   examined.  Alternatively (or in addition), you can define
     *   initNominalRoomPartLocation and initSpecialDesc - these work the same
     *   way, but will only be in effect until the object is moved.
     */
    class RoomPartItem: object
        /*
         *   show our special description when examining our associated room
         *   part, as long as we actually define a special description
         */
        useSpecialDescInRoomPart(part)
        {
            /* only show the special description in our associated room part */
            if (!isNominallyInRoomPart(part))
                return nil;

            /*
             *   if we define an initial special description, and this is our
             *   nominal room part for that description, use it
             */
            if (isInInitState
                && propType(&initSpecialDesc) != TypeNil
                && initNominalRoomPartLocation == part)
                return true;

            /* likewise for our specialDesc */
            if (propType(&specialDesc) != TypeNil
                && specialNominalRoomPartLocation == part)
                return true;

            /* otherwise, don't use the special description */
            return nil;
        }

        /*
         *   don't use the special description in room descriptions, or in
         *   examining any other container
         */
        useSpecialDescInRoom(room) { return nil; }
        useSpecialDescInContents(cont) { return nil; }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A Nested Room is any object that isn't a room but which can contain
     *   an actor: chairs, beds, platforms, vehicles, and the like.
     *
     *   An important property of nested rooms is that they're not
     *   full-fledged rooms for the purposes of actor arrivals and departures.
     *   Specifically, an actor moving from a room to a nested room within the
     *   room does not trigger an actor.travelTo invocation, but simply moves
     *   the actor from the containing room to the nested room.  Moving from
     *   the nested room to the containing room likewise triggers no
     *   actor.travelTo invocation.  The travelTo method is not applicable for
     *   intra-room travel because no TravelConnector objects are traversed in
     *   such travel; we simply move in and out of contained objects.  To
     *   mitigate this loss of notification, we instead call
     *   actor.travelWithin() when moving among nested locations.
     *
     *   By default, an actor attempting to travel from a nested location via
     *   a directional command will simply attempt the travel as though the
     *   actor were in the enclosing location.
     */
    class NestedRoom: BasicLocation
        /*
         *   Our interior room name.  This is the status line name we display
         *   when an actor is within this object and can't see out to the
         *   enclosing room.  Since we can't rely on the enclosing room's
         *   status line name if we can't see the enclosing room, we must
         *   provide one of our own.
         *
         *   By default, we'll use our regular name.
         */
        roomName = (name)

        /*
         *   Show our interior room description.  We use this to generate the
         *   long "look" description for the room when an actor is within the
         *   room and cannot see the enclosing room.
         *
         *   Note that this is used ONLY when the actor cannot see the
         *   enclosing room - when the enclosing room is visible (because the
         *   nested room is something like a chair that doesn't enclose the
         *   actor, or can enclose the actor but is open or transparent), then
         *   we'll simply use the description of the enclosing room instead,
         *   adding a note to the short name shown at the start of the room
         *   description indicating that the actor is in the nested room.
         *
         *   By default, we'll show the appropriate "actor here" description
         *   for the posture, so we'll say something like "You are sitting on
         *   the red chair" or "You are in the phone booth."  Instances can
         *   override this to customize the description with something more
         *   detailed, if desired.
         */
        roomDesc
        {
            local pov = getPOVActorDefault(gActor);
            pov.listActorPosture(pov);
        }

        /*
         *   The maximum bulk the room can hold.  We'll define this to a large
         *   number by default so that bulk isn't a concern.
         *
         *   Lower numbers here can be used, for example, to limit the seating
         *   capacity of a chair.
         */
        bulkCapacity = 10000

        /*
         *   Check for ownership.  For a nested room, an actor can be taken to
         *   own the nested room by virtue of being inside the room - the
         *   chair Bob is sitting in can be called "bob's chair".
         *
         *   If we don't have an explicit owner, and the potential owner 'obj'
         *   is in me and can own me, we'll report that 'obj' does in fact own
         *   me.  Otherwise, we'll defer to the inherited implementation.
         */
        isOwnedBy(obj)
        {
            /*
             *   if we're not explicitly owned, and 'obj' can own me, and
             *   'obj' is inside me, consider us owned by 'obj'
             */
            if (owner == nil && obj.isIn(self) && obj.canOwn(self))
                return true;

            /* defer to the inherited definition of ownership */
            return inherited(obj);
        }

        /*
         *   Get the extra scope items within this room.  Normally, the
         *   immediately enclosing nested room is in scope for an actor in the
         *   room.  So, if the actor is directly in 'self', return 'self'.
         */
        getExtraScopeItems(actor)
        {
            /* if the actor is directly in the nested room, return it */
            if (actor.isDirectlyIn(self))
                return [self];
            else
                return [];
        }

        /*
         *   By default, 'out' within a nested room should take us out of the
         *   nested room itself.  The easy way to accomplish this is to set up
         *   a 'nestedRoomOut' connector for the nested room, which will
         *   automatically try a GET OUT command.  If we didn't do this, we'd
         *   *usually* pick up the noTravelOut from our enclosing room, but
         *   only when the enclosing room didn't override 'out' to point
         *   somewhere else.  Explicitly setting up a 'noTravelOut' here
         *   ensures that we'll consistently GET OUT of the nested room even if
         *   the enclosing room has its own 'out' destination.
         *
         *   Note that nestedRoomOut shows as a listed exit in exit listings
         *   (for the EXITS command and in the status line).  If you don't want
         *   OUT to be listed as an available exit for the nested room, you
         *   should override this to use noTravelOut instead.
         */
        out = nestedRoomOut

        /*
         *   An actor is attempting to "get out" while in this location.  By
         *   default, we'll treat this as getting out of this object.  This
         *   can be overridden if "get out" should do something different.
         */
        disembarkRoom()
        {
            /* run the appropriate command to get out of this nested room */
            removeFromNested();
        }

        /*
         *   Make the actor stand up from this location.  By default, we'll
         *   cause the actor to travel (using travelWithin) to our container,
         *   and assume the appropriate posture for the container.
         */
        makeStandingUp()
        {
            /* remember the old posture, in case the travel fails */
            local oldPosture = gActor.posture;

            /* get the exit destination; if there isn't one, we can't proceed */
            local dest = exitDestination;
            if (dest == nil)
            {
                reportFailure(&cannotDoFromHereMsg);
                exit;
            }

            /*
             *   Set the actor's posture to the default for the destination.
             *   Do this before effecting the actual travel, so that the
             *   destination can change this default if it wants.
             */
            gActor.makePosture(dest.defaultPosture);

            /* protect against 'exit' and the like during the travel attempt */
            try
            {
                /*
                 *   move the actor to our exit destination, traveling entirely
                 *   within nested locations
                 */
                gActor.travelWithin(dest);
            }
            finally
            {
                /* if we didn't end up traveling, restore the old posture */
                if (gActor.isIn(self))
                    gActor.makePosture(oldPosture);
            }

            /* generate the appropriate default for the new location */
            gActor.okayPostureChange();
        }

        /*
         *   Try an implied command to move the actor from outside of this
         *   nested room into this nested room.  This must be overridden in
         *   subclasses to carry out the appropriate implied command.  Returns
         *   the result of tryImplicitAction().
         *
         *   This is called when we need to move an actor into this location
         *   as part of an implied command.  We use an overridable method
         *   because different kinds of nested rooms have different commands
         *   for entering: SIT ON CHAIR, LIE ON BED, GET IN CAR, RIDE BIKE,
         *   and so on.  This should be normally be overridden imply by
         *   calling tryImplicitAction() with the appropriate command for the
         *   specific type of nested room, and returning the result.
         */
        tryMovingIntoNested()
        {
            /* do nothing by default - subclasses must override */
            return nil;
        }

        /*
         *   message property to use for reportFailure when
         *   tryMovingIntoNested fails
         */
        mustMoveIntoProp = nil

        /*
         *   Try an implied command to remove an actor from this location and
         *   place the actor in my immediate containing location.  This must
         *   be overridden in subclasses to carry out the appropriate implied
         *   command.  Returns the result of tryImplicitAction().
         *
         *   This is essentially the reverse of tryMovingIntoNested(), and
         *   should in most cases be implemented by calling
         *   tryImplicitAction() with the appropriate command to get out of
         *   the room, and returning the result.
         */
        tryRemovingFromNested()
        {
            /* do nothing by default - subclasses must override */
            return nil;
        }

        /*
         *   Replace the current action with one that removes the actor from
         *   this nested room.  This is used to implement the GET OUT command
         *   when the actor is directly in this nested room.  In most cases,
         *   this should simply be implemented with a call to replaceAction()
         *   with the appropriate command.
         */
        removeFromNested()
        {
            /* subclasses must override */
        }

        /*
         *   Try moving the actor into this location.  This is used to move
         *   the actor into this location as part of meeting preconditions,
         *   and we use the normal precondition check protocol: we return nil
         *   if the condition (actor is in this room) is already met; we
         *   return true if we successfully execute an implied command to meet
         *   the condition; and we report a failure message and terminate the
         *   command with 'exit' if we don't know how to meet the condition or
         *   the implied command we try to execute fails or fails to satisfy
         *   the condition.
         *
         *   This does not normally need to be overridden in subclasses.
         */
        checkMovingActorInto(allowImplicit)
        {
            /* if the actor is within me, use default handling */
            if (gActor.isIn(self))
                return inherited(allowImplicit);

            /* try an implied command to move the actor into this nested room */
            if (allowImplicit && tryMovingIntoNested())
            {
                /* if we didn't succeed, terminate the command */
                if (!gActor.isDirectlyIn(self))
                    exit;

                /* tell the caller we executed an implied command */
                return true;
            }

            /*
             *   if we can be seen, report that the actor must travel here
             *   first; if we can't be seen, simply say that this can't be done
             *   from here
             */
            if (gActor.canSee(self))
            {
                /* report that we have to move into 'self' first */
                reportFailure(mustMoveIntoProp, self);
            }
            else
            {
                /*
                 *   we can't be seen; simply say this we can't do this command
                 *   from the current location
                 */
                reportFailure(&cannotDoFromHereMsg);
            }

            /* terminate the action */
            exit;
        }

        /*
         *   Check, using pre-condition rules, that the actor is removed from
         *   this nested location and moved to my immediate location.  This is
         *   used to enforce a precondition that the actor is in the enclosing
         *   location.
         *
         *   This isn't normally overridden in subclasses.
         */
        checkActorOutOfNested(allowImplicit)
        {
            /* try removing the actor from this nested location */
            if (allowImplicit && tryRemovingFromNested())
            {
                /*
                 *   make sure we managed to move the actor to our exit
                 *   destination
                 */
                if (!gActor.isDirectlyIn(exitDestination))
                    exit;

                /* indicate that we carried out an implied command */
                return true;
            }

            /* we can't carry out our implied departure plan - fail */
            reportFailure(&cannotDoFromMsg, self);
            exit;
        }

        /*
         *   Check, using pre-condition rules, that the actor is ready to
         *   enter this room as a nested location.
         *
         *   This isn't normally overridden in subclasses.
         */
        checkActorReadyToEnterNestedRoom(allowImplicit)
        {
            /*
             *   If the actor is directly in this room, we obviously need do
             *   nothing, as the actor is already in this nested room.
             */
            if (gActor.isDirectlyIn(self))
                return nil;

            /*
             *   If the actor isn't within us (directly or indirectly), we
             *   must move the actor to a valid "staging location," so that
             *   the actor can move from the staging location into us.  (A
             *   staging location is simply any location from which we can
             *   move directly into this nested room without any intervening
             *   travel in or out of other nested rooms.)
             */
            if (!gActor.isIn(self))
                return checkActorInStagingLocation(allowImplicit);

            /*
             *   The actor is within us, but isn't directly within us, so
             *   handle this with the normal routine to move the actor into
             *   this room.
             */
            return checkMovingActorInto(allowImplicit);
        }

        /*
         *   Check, using precondition rules, that the actor is in a valid
         *   "staging location" for entering this nested room.  We'll ensure
         *   that the actor is directly in one of the locations in our
         *   stagingLocations list, running an appropriate implicit command to
         *   move the actor to the first item in that list if the actor isn't
         *   in any of them.
         *
         *   This isn't normally overridden in subclasses.
         */
        checkActorInStagingLocation(allowImplicit)
        {
            local lst;
            local target;

            /* get the list of staging locations */
            lst = stagingLocations;

            /* if there are no valid staging locations, we can't move here */
            if (lst.length() == 0)
            {
                cannotMoveActorToStagingLocation();
                exit;
            }

            /*
             *   Try each of the locations in our staging list, to see if the
             *   actor is directly in any of them.
             */
            foreach (local cur in lst)
            {
                /*
                 *   if the actor is directly in this staging location, then
                 *   the actor can reach the destination with no additional
                 *   intervening travel - simply return nil in this case to
                 *   indicate that no implicit commands are needed before the
                 *   proposed nested room entry
                 */
                if (gActor.isDirectlyIn(cur))
                    return nil;
            }

            /*
             *   The actor isn't directly in any staging location, so we must
             *   move the actor to an appropriate staging location before we
             *   can proceed.  Choose a staging location based on the actor's
             *   current location.
             */
            if ((target = chooseStagingLocation()) != nil)
            {
                /*
                 *   We've chosen a target staging location.  First, check to
                 *   make sure the location we've chosen is valid - we might
                 *   have chosen a default (such as the nested room's
                 *   immediate container) that isn't usable as a staging
                 *   location, so we need to check with it first to make sure
                 *   it's willing to allow this.
                 */
                target.checkStagingLocation(self);

                /*
                 *   The check routine didn't abort the command, so try an
                 *   appropriate implicit command to move the actor into the
                 *   chosen staging location.
                 */
                return target.checkMovingActorInto(allowImplicit);
            }

            /*
             *   There's no apparent intermediate staging location given the
             *   actor's current location.  We thus cannot proceed with the
             *   command; simply report that we can't get there from here.
             */
            cannotMoveActorToStagingLocation();
            exit;
        }

        /*
         *   Choose an intermediate staging location, given the actor's
         *   current location.  This routine is called when the actor is
         *   attempting to move into 'self', but isn't in any of the allowed
         *   staging locations for 'self'; this routine's purpose is to choose
         *   the staging location that the actor should implicitly try to
         *   reach on the way to 'self'.
         *
         *   By default, we'll attempt to find the first of our staging
         *   locations that indirectly contains the actor.  (We know none of
         *   the staging locations directly contains the actor, because if one
         *   did, we wouldn't be called in the first place - we're only called
         *   when the actor isn't already directly in one of our staging
         *   locations.)  This approach is appropriate when nested rooms are
         *   related purely by containment: if an actor is in a nested room
         *   within one of our staging locations, we can reach that staging
         *   location by having the actor get out of the more deeply nested
         *   room.
         *
         *   However, this default approach is not appropriate when nested
         *   rooms are related in some way other than simple containment.  We
         *   don't have any general framework for other types of nested room
         *   relationships, so this routine must be overridden in such a case
         *   with special-purpose code defining the special relationship.
         *
         *   If we fail to find any staging location indirectly containing the
         *   actor, we'll return the result of defaultStagingLocation().
         */
        chooseStagingLocation()
        {
            /* look for a staging location indirectly containing the actor */
            foreach (local cur in stagingLocations)
            {
                /*
                 *   if the actor is indirectly in this staging location,
                 *   choose it as the target intermediate staging location
                 */
                if (gActor.isIn(cur))
                    return cur;
            }

            /*
             *   We didn't find any locations in the staging list that
             *   indirectly contain the actor, so use the default staging
             *   location.
             */
            return defaultStagingLocation();
        }

        /*
         *   The default staging location for this nested room.  This is the
         *   staging location we'll attempt to reach implicitly if the actor
         *   isn't in any of the rooms in the stagingLocations list already.
         *   We'll return the first element of our stagingLocations list for
         *   which isStagingLocationKnown returns true.
         */
        defaultStagingLocation()
        {
            local lst;

            /* get the list of valid staging locations */
            lst = stagingLocations;

            /* find the first element which is known to the actor */
            foreach (local cur in lst)
            {
                /* if this staging location is known, take it as the default */
                if (isStagingLocationKnown(cur))
                    return cur;
            }

            /* we didn't find any known staging locations - there's no default */
            return nil;
        }

        /*
         *   Report that we are unable to move an actor to any staging
         *   location for this nested room.  By default, we'll generate the
         *   message "you can't do that from here," but this can overridden to
         *   provide a more specific if desired.
         */
        cannotMoveActorToStagingLocation()
        {
            /* report the standard "you can't do that from here" message */
            reportFailure(&cannotDoFromHereMsg);
        }

        /*
         *   Report that we are unable to move an actor out of this nested
         *   room, because there's no valid 'exit destination'.  This is
         *   called when we attempt to GET OUT OF the nested room, and the
         *   'exitDestination' property is nil.
         */
        cannotMoveActorOutOf()
        {
            /* report the standard "you can't do that from here" message */
            reportFailure(&cannotDoFromHereMsg);
        }

        /*
         *   The valid "staging locations" for this nested room.  This is a
         *   list of the rooms from which an actor can DIRECTLY reach this
         *   nested room; in other words, the actor will be allowed to enter
         *   'self', with no intervening travel, if the actor is directly in
         *   any of these locations.
         *
         *   If the list is empty, there are no valid staging locations.
         *
         *   The point of listing staging locations is to make certain that
         *   the actor has to go through one of these locations in order to
         *   get into this nested room.  This ensures that we enforce any
         *   conditions or trigger any side effects of moving through the
         *   staging locations, so that a player can't bypass a puzzle by
         *   trying to move directly from one location to another without
         *   going through the required intermediate steps.  Since we always
         *   require that an actor go through one of our staging locations in
         *   order to enter this nested room, and since we carry out the
         *   travel to the staging location using implied commands (which are
         *   just ordinary commands, entered and executed automatically by the
         *   parser), we can avoid having to code any checks redudantly in
         *   both the staging locations and any other nearby locations.
         *
         *   By default, an actor can only enter a nested room from the room's
         *   direct container.  For example, if a chair is on a stage, an
         *   actor must be standing on the stage before the actor can sit on
         *   the chair.
         */
        stagingLocations = [location]

        /*
         *   Our exit destination.  This is where an actor ends up when the
         *   actor is immediately inside this nested room and uses a "get out
         *   of" or equivalent command to exit the nested room.
         *
         *   By default, we'll use the default staging location as the exit
         *   destination.
         */
        exitDestination = (defaultStagingLocation())

        /*
         *   Is the given staging location "known"?  This returns true if the
         *   staging location is usable as a default, nil if not.  If this
         *   returns true, then the location can be used in an implied command
         *   to move the actor to the staging location in order to move the
         *   actor into self.
         *
         *   If this returns nil, no implied command will be attempted for
         *   this possible staging location.  This doesn't mean that an actor
         *   gets a free pass through the staging location; on the contrary,
         *   it simply means that we won't try any automatic command to move
         *   an actor to the staging location, hence travel from a non-staging
         *   location to this nested room will simply fail.  This can be used
         *   when part of the puzzle is to figure out that moving to the
         *   staging location is required in the first place: if we allowed an
         *   implied command in such cases, we'd give away the puzzle by
         *   solving it automatically.
         *
         *   By default, we'll treat all of our staging locations as known.
         */
        isStagingLocationKnown(loc) { return true; }

        /*
         *   Get the travel preconditions for an actor in this location.  By
         *   default, if we have a container, and the actor can see the
         *   container, we'll return its travel preconditions; otherwise, we'll
         *   use our inherited preconditions.
         */
        roomTravelPreCond()
        {
            local ret;

            /*
             *   If we can see out to our location, use the location's
             *   conditions, since by default we'll try traveling from the
             *   location; if we can't see out to our location, we won't be
             *   attempting travel through our location's connectors, so use
             *   our own preconditions instead.
             */
            if (location != nil && gActor.canSee(location))
                ret = location.roomTravelPreCond();
            else
                ret = inherited();

            /* return the results */
            return ret;
        }

        /*
         *   We cannot take a nested room that the actor is occupying
         */
        dobjFor(Take)
        {
            verify()
            {
                /* it's illogical to take something that contains the actor */
                if (gActor.isIn(self))
                    illogicalNow(&cannotTakeLocationMsg);

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

        /*
         *   "get out of" action - exit the nested room
         */
        dobjFor(GetOutOf)
        {
            preCond()
            {
                return [new ObjectPreCondition(self, actorDirectlyInRoom)];
            }
            verify()
            {
                /*
                 *   the actor must be located on the platform; but allow the
                 *   actor to be indirectly on the platform, since we'll use a
                 *   precondition to move the actor out of any more nested
                 *   rooms within us
                 */
                if (!gActor.isIn(self))
                    illogicalNow(&notOnPlatformMsg);
            }
            check()
            {
                /*
                 *   If we have no 'exit destination' - that is, we have
                 *   nowhere to go when we GET OUT OF the nested room - then
                 *   prohibit the operation.
                 */
                if (exitDestination == nil)
                {
                    /* explain the problem and terminate the command */
                    cannotMoveActorOutOf();
                    exit;
                }
            }
            action()
            {
                /* travel to our get-out-of destination */
                gActor.travelWithin(exitDestination);

                /*
                 *   set the actor's posture to the default posture for the
                 *   new location
                 */
                gActor.makePosture(gActor.location.defaultPosture);

                /* issue a default report of the change */
                defaultReport(&okayNotStandingOnMsg);
            }
        }

        /* explicitly define the push-travel indirect object mappings */
        mapPushTravelIobj(PushTravelGetOutOf, TravelVia)
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A "high nested room" is a nested room that is elevated above the rest
     *   of the room.  This specializes the staging location handling so that
     *   it generates more specific messages.
     */
    class HighNestedRoom: NestedRoom
        /* report that we're unable to move to a staging location */
        cannotMoveActorToStagingLocation()
        {
            reportFailure(&nestedRoomTooHighMsg, self);
        }

        /* if we can't get out, report that it's because we're too high up */
        cannotMoveActorOutOf()
        {
            reportFailure(&nestedRoomTooHighToExitMsg, self);
        }

        /*
         *   Staging locations.  By default, we'll return an empty list,
         *   because a high location is not usually reachable directly from its
         *   containing location.
         *
         *   Note that puzzles involving moving platforms will have to manage
         *   this list dynamically, which could be done either by writing a
         *   method here that returns a list of currently valid staging
         *   locations, or by adding objects to this list as they become valid
         *   staging locations and removing them when they cease to be.  For
         *   example, if we have an air vent in the ceiling that we can only
         *   reach when a chair is placed under the vent, this property could
         *   be implemented as a method that returns a list containing the
         *   chair only when the chair is in the under-the-vent state.
         *
         *   Note that this empty default setting will also give us no exit
         *   destination, since the default exit location is the default
         *   staging location.
         */
        stagingLocations = []
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A chair is an item that an actor can sit on.  When an actor is sitting
     *   on a chair, the chair contains the actor.  In addition to sitting,
     *   chairs can optionally allow standing as well.
     *
     *   We define the "BasicChair" as something that an actor can sit on, and
     *   then subclass this with the standard "Chair", which adds surface
     *   capabilities.
     */
    class BasicChair: NestedRoom
        /*
         *   A list of the allowed postures for this object.  By default, we
         *   can sit and stand on a chair, since most ordinary chairs are
         *   suitable for both.
         */
        allowedPostures = [sitting, standing]

        /*
         *   A list of the obvious postures for this object.  The only obvious,
         *   default thing you do with most ordinary chairs is sit on them,
         *   even they allow other postures.  Something like a large sofa might
         *   want to allow both sitting and lying.
         *
         *   This list differs from the allowed postures list because some
         *   postures might be possible but not probable.  For most ordinary
         *   chairs, standing is possible, but it's not the first thing you'd
         *   think of doing with the chair.
         */
        obviousPostures = [sitting]

        /*
         *   A chair's effective follow location is usually its location's
         *   effective follow location, because we don't usually want to treat
         *   a chair as a separate location for the purposes of "follow."
         *   That is, if A and B are in the same room, and A sits down on a
         *   chair in the room, we don't want to count this as a move that B
         *   could follow.
         */
        effectiveFollowLocation = (location.effectiveFollowLocation)

        /*
         *   Try an implied command to move the actor from outside of this
         *   nested room into this nested room.  By default, we'll call upon
         *   our default posture object to activate its command to move the
         *   actor into this object in the default posture.  For a chair, the
         *   default posture is typically sitting, so the 'sitting' posture
         *   will perform a SIT ON <self> command.
         */
        tryMovingIntoNested()
        {
            /*
             *   ask our default posture object to carry out the appropriate
             *   command to move the actor into 'self' in that posture
             */
            return defaultPosture.tryMakingPosture(self);
        }

        /* tryMovingIntoNested failure message is "must sit on chair" */
        mustMoveIntoProp = &mustSitOnMsg

        /* default posture in this nested room is sitting */
        defaultPosture = sitting

        /*
         *   by default, objects dropped while sitting in a chair go into the
         *   enclosing location's drop destination
         */
        getDropDestination(obj, path)
        {
            return location != nil
                ? location.getDropDestination(obj, path)
                : self;
        }

        /*
         *   Remove an actor from the chair.  By default, we'll simply stand
         *   up, since this is the normal way out of a chair.
         */
        tryRemovingFromNested()
        {
            /* try standing up */
            if (gActor.posture == sitting)
                return tryImplicitAction(Stand);
            else
                return tryImplicitAction(GetOffOf, self);
        }

        /*
         *   Run the appropriate command to remove us from this nested
         *   container, as a replacement command.
         */
        removeFromNested()
        {
            /* to get out of a chair, we simply stand up */
            if (gActor.posture == sitting)
                replaceAction(Stand);
            else
                replaceAction(GetOutOf, self);
        }

        /*
         *   "sit on" action
         */
        dobjFor(SitOn)
        {
            preCond = (preCondForEntry(sitting))
            verify()
            {
                /* verify entering the chair in a 'sitting' posture */
                if (verifyEntry(sitting, &alreadySittingOnMsg, &noRoomToSitMsg))
                    inherited();
            }
            action()
            {
                /* enter the chair in the 'sitting' posture */
                performEntry(sitting);
            }
        }

        /*
         *   "stand on" action
         */
        dobjFor(StandOn)
        {
            /* if it's allowed, use the same preconditions as 'sit on' */
            preCond = (preCondForEntry(standing))

            verify()
            {
                /* verify entering in a 'standing' posture */
                if (verifyEntry(standing, &alreadyStandingOnMsg,
                                &noRoomToStandMsg))
                    inherited();
            }
            action()
            {
                /* enter the chair in the 'standing' posture */
                performEntry(standing);
            }
        }

        /*
         *   "lie on" action
         */
        dobjFor(LieOn)
        {
            preCond = (preCondForEntry(lying))
            verify()
            {
                /* verify entering in a 'lying' posture */
                if (verifyEntry(lying, &alreadyLyingOnMsg, &noRoomToLieMsg))
                    inherited();
            }
            action()
            {
                /* enter in the 'lying' posture */
                performEntry(lying);
            }
        }

        /*
         *   For "get on/in" / "board", let our default posture object handle
         *   it, by running the appropriate nested action that moves the actor
         *   into self in the default posture.
         */
        dobjFor(Board)
        {
            verify() { }
            action() { defaultPosture.setActorToPosture(gActor, self); }
        }

        /* "get off of" is the same as "get out of" */
        dobjFor(GetOffOf) asDobjFor(GetOutOf)

        /* standard preconditions for sitting/lying/standing on the chair */
        preCondForEntry(posture)
        {
            /*
             *   if this is not among my allowed postures, we don't need any
             *   special preconditions, since we'll fail in the verify
             */
            if (allowedPostures.indexOf(posture) == nil)
                return inherited();

            /*
             *   in order to enter the chair, we have to be able to touch it
             *   and we have to be able to enter it as a nested room
             */
            return [touchObj,
                    new ObjectPreCondition(self, actorReadyToEnterNestedRoom)];
        }

        /*
         *   Verify that we can enter the chair in the given posture.  This
         *   performs verification work common to SIT ON, LIE ON, and STAND ON.
         *   If this returns true, the caller should inherit the base class
         *   default handling, otherwise it shouldn't.
         */
        verifyEntry(posture, alreadyMsg, noRoomMsg)
        {
            /*
             *   if the given posture isn't allowed, tell the caller to use the
             *   inherited default handling
             */
            if (allowedPostures.indexOf(posture) == nil)
                return true;

            /* this posture is allowed, but it might not be obvious */
            if (obviousPostures.indexOf(posture) == nil)
                nonObvious;

            /*
             *   If the actor is already on this chair in the given posture,
             *   this action is redundant.  If we already verified okay on this
             *   point for this same action, ignore the repeated command - it
             *   must mean that we applied a precondition that did all of our
             *   work for us (such as moving us out of a nested room
             *   immediately within us).
             */
            if (gActor.posture == posture && gActor.isDirectlyIn(self)
                && gAction.verifiedOkay.indexOf(self) == nil)
                illogicalNow(alreadyMsg);
            else
                gAction.verifiedOkay += self;

            /*
             *   If there's not room for the actor's added bulk, don't allow
             *   the actor to sit/lie/stand on the chair.  If the actor is
             *   already within the chair, there's no need to add the actor's
             *   bulk for this change, since it's already counted as being
             *   within us.
             */
            if (!gActor.isIn(self)
                && getBulkWithin() + gActor.getBulk() > bulkCapacity)
                illogicalNow(noRoomMsg);

            /* we can't sit/stand/lie on something the actor is holding */
            if (isIn(gActor))
                illogicalNow(&cannotEnterHeldMsg);

            /*
             *   if the actor is already in me, but in a different posture,
             *   boost the likelihood slightly
             */
            if (gActor.isDirectlyIn(self) && gActor.posture != posture)
                logicalRank(120, 'already in');

            /* tell the caller we don't want to inherit the base class handling */
            return nil;
        }

        /*
         *   Perform entry in the given posture.  This carries out the common
         *   actions for SIT ON, LIE ON, and STAND ON.
         */
        performEntry(posture)
        {
            /*
             *   Move the actor into me - this counts as interior travel within
             *   the enclosing room.  Note that we move the actor before
             *   changing the actor's posture in case the travel fails.
             */
            gActor.travelWithin(self);

            /* set the actor to the desired posture */
            gActor.makePosture(posture);

            /* report success */
            defaultReport(&roomOkayPostureChangeMsg, posture, self);
        }
    ;

    /*
     *   A Chair is a basic chair with the addition of being a Surface.
     */
    class Chair: BasicChair, Surface
        /*
         *   By default, a chair has a seating capacity of one person, so use
         *   a maximum bulk that only allows one actor to occupy the chair at
         *   a time.
         */
        bulkCapacity = 10
    ;

    /*
     *   Bed.  This is an extension of Chair that allows actors to lie on it
     *   as well as sit on it.  As with chairs, we have a basic bed, plus a
     *   regular bed that serves as a surface as well.
     */
    class BasicBed: BasicChair
        /*
         *   we can sit, lie, and stand on a typical bed, but only sitting and
         *   lying are obvious default actions
         */
        allowedPostures = [sitting, lying, standing]
        obviousPostures = [sitting, lying]

        /* tryMovingIntoNested failure message is "must sit on chair" */
        mustMoveIntoProp = &mustLieOnMsg

        /* default posture in this nested room is sitting */
        defaultPosture = lying
    ;

    /*
     *   A Bed is a basic bed with the addition of Surface capabilities.
     */
    class Bed: BasicBed, Surface
    ;

    /*
     *   A Platform is a nested room upon which an actor can stand.  In
     *   general, when you can stand on something, you can also sit and lie on
     *   it as well (it might not be comfortable, but it is usually at least
     *   possible), so we make this a subclass of Bed.
     *
     *   The main difference between a platform and a chair that allows
     *   standing is that a platform is more of a mini-room.  In particular,
     *   items an actor drops while standing on a platform land on the platform
     *   itself, whereas items dropped while sitting (or standing) on a chair
     *   land in the enclosing room.  In addition, the obvious default action
     *   for a chair is to sit on it, while the obvious default action for a
     *   platform is to stand on it.
     */
    class BasicPlatform: BasicBed
        /*
         *   we can sit, lie, and stand on a typical platform, and all of
         *   these could be reasonably expected to be done
         */
        allowedPostures = [sitting, lying, standing]
        obviousPostures = [sitting, lying, standing]

        /* an actor can follow another actor onto or off of a platform */
        effectiveFollowLocation = (self)

        /* tryMovingIntoNested failure message is "must get on platform" */
        mustMoveIntoProp = &mustGetOnMsg

        /* default posture in this nested room is sitting */
        defaultPosture = standing

        /* by default, objects dropped on a platform go onto the platform */
        getDropDestination(obj, path)
        {
            return self;
        }

        /*
         *   Remove an actor from the platform.  "Get off" is the normal
         *   command to leave a platform.
         */
        tryRemovingFromNested()
        {
            /* try getting off of the platform */
            return tryImplicitAction(GetOffOf, self);
        }

        /*
         *   Replace the current action with one that removes the actor from
         *   this nested room.
         */
        removeFromNested()
        {
            /* get off of the platform */
            replaceAction(GetOffOf, self);
        }

        /*
         *   Make the actor stand up.  On a platform, standing is normally
         *   allowed, so STAND doesn't usually imply "get off platform" as it
         *   does in the base class.
         */
        makeStandingUp()
        {
            /*
             *   If standing isn't among my allowed postures, inherit the
             *   default behavior, which is to get out of the nested room.
             */
            if (allowedPostures.indexOf(standing) == nil)
            {
                /* we can't stand on the platform, so use the default handling */
                inherited();
            }
            else
            {
                /* we can stand on the platform, so make the actor stand */
                gActor.makePosture(standing);

                /* issue a default report of the change */
                defaultReport(&roomOkayPostureChangeMsg, standing, self);
            }
        }

        /*
         *   Traveling 'down' from a platform should generally be taken to
         *   mean 'get off platform'.
         */
        down = noTravelDown
    ;

    /*
     *   A Platform is a basic platform with the addition of Surface behavior.
     */
    class Platform: BasicPlatform, Surface
    ;

    /*
     *   A "nominal platform" is a named place where NPC's can stand.  This
     *   class makes it easy to arrange for an NPC to be described as standing
     *   in a particular location in the room: for example, we could have an
     *   actor "standing in a doorway", or "leaning against the streetlamp."
     *
     *   In most cases, a nominal platform is a "secret" object, in that it
     *   won't be listed in a room's contents and it won't have any vocabulary
     *   words.  So, the player will never be able to refer to the object in a
     *   command.
     *
     *   To use this class, instantiate it with an object located in the room
     *   containing the pseudo-platform.  Don't give the object any vocabulary
     *   words.  Locate actors within the pseudo-platform to give them the
     *   special description.
     *
     *   For simple platform-like "standing on" descriptions, just define the
     *   name.  For descriptions like "standing in", "standing under", or
     *   "standing near", where only the preposition needs to be customized,
     *   define the name and define actorInPrep.  For more elaborate
     *   customizations, such as "leaning against the streetlamp", you'll need
     *   to override roomActorHereDesc, roomActorStatus, roomActorPostureDesc,
     *   roomListActorPosture, and actorInGroupPrefix/Suffix,
     */
    class NominalPlatform: Fixture, Platform
        /* don't let anyone stand/sit/lie here via a command */
        dobjFor(StandOn) { verify() { illogical(&cannotStandOnMsg); } }
        dobjFor(SitOn) { verify() { illogical(&cannotSitOnMsg); } }
        dobjFor(LieOn) { verify() { illogical(&cannotLieOnMsg); } }

        /* ignore me for 'all' and object defaulting */
        hideFromAll(action) { return true; }
        hideFromDefault(action) { return true; }

        /*
         *   nominal platforms are internal objects only, not part of the
         *   visible game world structure, so treat them as equivalent to their
         *   location for FOLLOW purposes
         */
        effectiveFollowLocation = (location.effectiveFollowLocation)
    ;

    /*
     *   A booth is a nested room that serves as a small enclosure within a
     *   larger room.  Booths can serve as regular containers as well as
     *   nested rooms, and can be made openable by addition of the Openable
     *   mix-in class.  Note that booths don't have to be fully enclosed, nor
     *   do they actually have to be closable.
     *
     *   Examples of booths: a cardboard box large enough for an actor can
     *   stand in; a closet; a shallow pit.
     */
    class Booth: BasicPlatform, Container
        /*
         *   Try an implied command to move the actor from outside of this
         *   nested room into this nested room.
         */
        tryMovingIntoNested()
        {
            /* try getting in me */
            return tryImplicitAction(Board, self);
        }

        /*
         *   Remove an actor from the booth.  "Get out" is the normal command
         *   to leave this type of room.
         */
        tryRemovingFromNested()
        {
            /* try getting out of the object */
            return tryImplicitAction(GetOutOf, self);
        }

        /*
         *   Replace the current action with one that removes the actor from
         *   this nested room.
         */
        removeFromNested()
        {
            /* get out of the object */
            replaceAction(GetOutOf, self);
        }

        /*
         *   "Enter" is equivalent to "get in" (or "board") for a booth
         */
        dobjFor(Enter) asDobjFor(Board)

        /* explicitly define the push-travel indirect object mapping */
        mapPushTravelIobj(PushTravelEnter, Board)
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A Vehicle is a special type of nested room that moves instead of the
     *   actor in response to travel commands.  When an actor in a vehicle
     *   types, for example, "go north," the vehicle moves north, not the
     *   actor.
     *
     *   In most cases, a Vehicle should multiply inherit from one of the
     *   other nested room subclasses to make it more specialized.  For
     *   example, a bicycle might inherit from Chair, so that actors can sit
     *   on the bike.
     *
     *   Note that because Vehicle inherits from NestedRoom, the OUT direction
     *   in the vehicle by default means what it does in NestedRoom -
     *   specifically, getting out of the vehicle.  This is appropriate for
     *   vehicles where we'd describe passengers as being inside the vehicle,
     *   such as a car or a boat.  However, if the vehicle is something you
     *   ride on, like a horse or a bike, it's probably more appropriate for
     *   OUT to mean "ride the vehicle out of the enclosing room."  To get
     *   this effect, simply override the "out" property and set it to nil;
     *   this will prevent the NestedRoom definition from being inherited,
     *   which will make us look for the OUT location of the enclosing room as
     *   the travel destination.
     */
    class Vehicle: NestedRoom, Traveler
        /*
         *   When a traveler is in a vehicle, and the traveler performs a
         *   travel command, the vehicle is what changes location; the
         *   contained traveler simply stays put while the vehicle moves.
         */
        getLocTraveler(trav, conn)
        {
            local stage;

            /*
             *   If the connector is contained within the vehicle, take it as
             *   leading out of the vehicle - this means we want to move
             *   traveler within the vehicle rather than the vehicle.
             *   Consider the connector be inside the vehicle if it's a
             *   physical object (a Thing) that's inside the vehicle, or one
             *   of the vehicle's own directional links points directly to the
             *   connector.
             *
             *   Likewise, if the connector is marked with the vehicle or any
             *   object inside the vehicle as its staging location, the
             *   vehicle obviously isn't involved in the travel.
             */
            if ((conn != nil && conn.ofKind(Thing) && conn.isIn(self))
                || Direction.allDirections.indexWhich(
                    {dir: self.(dir.dirProp) == conn}) != nil
                || ((stage = conn.connectorStagingLocation) != nil
                    && (stage == self || stage.isIn(self))))
            {
                /*
                 *   this connector leads out from within the vehicle - move
                 *   the inner traveler rather than the vehicle itself
                 */
                return trav;
            }

            /*
             *   If we have a location, ask it who travels when the VEHICLE is
             *   the traveler within it; otherwise, the vehicle is the
             *   traveler.
             *
             *   We ask the location, because the location might itself be a
             *   vehicle, in which case it might want us to be driving around
             *   the enclosing vehicle.  However, we pass ourselves (i.e.,
             *   this vehicle) as the inner traveler, rather than the traveler
             *   we were passed, because a traveler within a vehicle moves the
             *   vehicle when traveling.
             */
            return (location != nil ? location.getLocTraveler(self, conn) : self);
        }

        /*
         *   An OUT command while within a vehicle could mean one of two
         *   things: either to GET OUT of the vehicle, or to ride/drive the
         *   vehicle out of its enclosing location.
         *
         *   There's no good way of guessing which meaning the player intends,
         *   so we have to choose one or the other.  We choose the ride/drive
         *   interpretation as the default, for two reasons.  First, it seems
         *   to be what most players expect.  Second, the other interpretation
         *   leaves no standard way of expressing the ride/drive meaning.  We
         *   return nil here to indicate to the OUT action that we want the
         *   enclosing location's 'out' connector to be used while an actor is
         *   in the vehicle.
         *
         *   For some vehicles, it might be more appropriate for OUT to mean
         *   GET OUT.  In these cases, simply override this so that it returns
         *   nestedRoomOut.
         */
        out = nil

        /*
         *   Get the "location push traveler" - this is the traveler when a
         *   push-travel command is performed by a traveler within this
         *   location.  If the object we're trying to push is within me, use
         *   the contained traveler, since the contained traveler must be
         *   trying to push the object around directly.  If the object isn't
         *   inside me, then we're presumably trying to use the vehicle to push
         *   around the object, so the traveler is the vehicle or something
         *   containing the vehicle.
         */
        getLocPushTraveler(trav, obj)
        {
            /*
             *   If the object is inside me, use the nested traveler;
             *   otherwise, we're presumably trying to use the vehicle to move
             *   the object.
             */
            if (obj.isIn(self))
            {
                /*
                 *   we're moving something around inside me; use the
                 *   contained traveler
                 */
                return trav;
            }
            else if (location != nil)
            {
                /*
                 *   we're pushing something around outside me, so we're
                 *   probably trying to use the vehicle to do so; we have a
                 *   location, so ask it what it thinks, passing myself as the
                 *   new suggested traveler
                 */
                return location.getLocPushTraveler(self, obj);
            }
            else
            {
                /*
                 *   we're pushing something around outside me, and I have no
                 *   location, so I must be the traveler
                 */
                return self;
            }
        }

        /*
         *   Determine if an actor is traveling with me.  The normal base
         *   class implementation works, but it's more efficient just to check
         *   to see if the actor is inside this object than to construct the
         *   entire nested contents list just to check to see if the actor's
         *   in that list.
         */
        isActorTraveling(actor) { return actor.isIn(self); }

        /* invoke a callback for each actor traveling with us */
        forEachTravelingActor(func)
        {
            /* invoke the callback on each actor in our contents */
            allContents().forEach(function(obj) {
                if (obj.isActor)
                    (func)(obj);
            });
        }

        /*
         *   Get the actors involved in the travel.  This is a list consisting
         *   of all of the actors contained within the vehicle.
         */
        getTravelerActors = (allContents().subset({x: x.isActor}))

        /*
         *   there are no self-motive actors in a vehicle - the vehicle is
         *   doing the travel, and the actors within are just moving along
         *   with it as cargo
         */
        getTravelerMotiveActors = []

        /*
         *   Traveler preconditions for the vehicle.  By default, we add no
         *   preconditions of our own, but specific vehicles might want to
         *   override this.  For example, a car might want to require that the
         *   doors are closed, the engine is running, and the seatbelts are
         *   fastened before it can travel.
         */
        travelerPreCond(conn) { return []; }

        /*
         *   Check, using pre-condition rules, that the traveler is in the
         *   given room, moving the traveler to the room if possible.
         */
        checkMovingTravelerInto(room, allowImplicit)
        {
            /* if we're in the desired location, we're set */
            if (isDirectlyIn(room))
                return nil;

            /*
             *   By default, we can't move a vehicle into a room implicitly.
             *   Individual vehicles can override this when there's an obvious
             *   way of moving the vehicle in and out of nested rooms.
             */
            reportFailure(&vehicleCannotDoFromMsg, self);
            exit;
        }

        /*
         *   the lister object we use to display the list of actors aboard, in
         *   arrival and departure messages for the vehicle
         */
        aboardVehicleListerObj = aboardVehicleLister
    ;

    /*
     *   A VehicleBarrier is a TravelConnector that allows actors to travel,
     *   but blocks vehicles.  By default, we block all vehicles, but
     *   subclasses can customize this so that we block only specific
     *   vehicles.
     */
    class VehicleBarrier: TravelBarrier
        /*
         *   Determine if the given traveler can pass through this connector.
         *   By default, we'll return nil for a Vehicle, true for anything
         *   else.  This can be overridden to allow specific vehicles to pass,
         *   or to filter on any other criteria.
         */
        canTravelerPass(traveler) { return !traveler.ofKind(Vehicle); }

        /* explain why we can't pass */
        explainTravelBarrier(traveler)
        {
            reportFailure(&cannotGoThatWayInVehicleMsg, traveler);
        }
    ;

TADS 3 Library Manual
Generated on 5/16/2013 from TADS version 3.1.3