#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"


/* ------------------------------------------------------------------------ */
/*
 *   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
    /* 
     *   "Exit" action - this simply maps to travel via the connector.  
     */
    dobjFor(GetOutOf) remapTo(TravelVia, self)

    /* explicitly define the push-travel indirect object mapping */
    mapPushTravelIobj(PushTravelExit, 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)
            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);
    }

    /*
     *   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 ((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)
    {
        local loc;
        
        /* 
         *   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.  
         */
        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(PushTravelOutOf, 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(new 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);
    }
;

