travel.t

documentation
 #charset "us-ascii"
 
 /* 
  *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved. 
  *   
  *   TADS 3 Library - travel
  *   
  *   This module defines the parts of the simulation model related to
  *   travel: rooms and other locations, directions, passages.  
  */
 
 /* include the library header */
 #include "adv3.h"
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   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)});
     }
 
     /*
      *   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();
 
             /* 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);
     }
 
     /*
      *   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);
     }
 
     /*
      *   Get the apparent location of one of our room parts (the floor, the
      *   ceiling, etc).  By default, we don't have any room parts, so we
      *   return nil.  
      */
     getRoomPartLocation(part) { return nil; }
 
     /*
      *   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. 
      */
     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)
 
     /*
      *   Get the *location* traveler - this is the object that's actually
      *   going to change location when a traveler within this location
      *   performs a travel command to travel via the given connector.  By
      *   default, this is simply the contained traveler.  This can be
      *   overridden when some other object is to be the traveler; for
      *   example, vehicles override this to make the vehicle travel on
      *   behalf of a traveler within.  
      */
     getLocTraveler(trav, conn) { return trav; }
 
     /*
      *   Get the "location push traveler" - this is the object that's going
      *   to travel for a push-travel action performed by a traveler within
      *   this location.  This is called by a traveler within this location
      *   to find out if the location wants to be involved in the travel, as
      *   a vehicle might be.
      *   
      *   By default, this is simply 'trav', the original traveler.  'obj'
      *   is the object we're trying to push.  
      */
     getLocPushTraveler(trav, obj) { return trav; }
 
     /*
      *   Receive notifiation 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)
 
     /*
      *   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();
     }
 
     /*
      *   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
 
     /* 
      *   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 
              */
             if (gActor.isIn(self))
                 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, we're already here */
     dobjFor(Board) asDobjFor(Enter)
     dobjFor(Enter) { verify() { illogicalAlready(&alreadyInLocMsg); } }
 ;
 
 /*
  *   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 { getPOVActor().listActorPosture(getPOVActor()); }
 
     /*
      *   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, let the containing room handle the room daemon */
     roomDaemon()
     {
         if (location != nil)
             location.roomDaemon();
     }
 
     /*
      *   Our atmospheric message list.  By default, if our container is
      *   visible to us, we'll use our container's atmospheric messages.
      *   This can be overridden to provide our own atmosphere list when
      *   the player character is in this nested room.  
      */
     atmosphereList()
     {
         if (location != nil && gPlayerChar.canSee(location))
             return location.atmosphereList;
         else
             return nil;
     }
 
     /* 
      *   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();
     }
 
     /*
      *   Get the apparent location of one of our room parts (the floor,
      *   the ceiling, etc).  By default, we'll simply ask our container
      *   about it, since a nested room by default doesn't have any of the
      *   standard room parts.  
      */
     getRoomPartLocation(part)
     {
         if (location != nil)
             return location.getRoomPartLocation(part);
         else
             return nil;
     }
 
     /*
      *   Get the drop destination - this is the location where an object
      *   goes when an actor is in this room and drops the object.  By
      *   default, objects dropped in a nested room land in the nested room.
      *   Some types of nested rooms might want to override this; for
      *   example, a nested room that doesn't enclose its actor, such as a
      *   chair, might want to send dropped items to the enclosing room.  
      */
     getDropDestination(objToDrop, path)
     {
         return self;
     }
 
     /*
      *   Get the location traveler - this is the object that's actually
      *   going to change location when a traveler within this location
      *   performs a travel command to travel via the given connector.  By
      *   default, we'll indicate what our containing room indicates.  (The
      *   enclosing room might be a vehicle or an ordinary room; in any
      *   case, it'll know what to do, so we merely have to ask it.)
      *   
      *   We defer to our enclosing room by default because this allows for
      *   things like a seat in a car: the actor is sitting in the seat and
      *   starts traveling in the car, so the seat calls the enclosing room,
      *   which is the car, and the car returns itself, since it's the car
      *   that will be traveling.  
      */
     getLocTraveler(trav, conn)
     {
         /* 
          *   ask our location if we have one; otherwise, it's the contained
          *   traveler that moves by default 
          */
         return (location != nil ? location.getLocTraveler(trav, conn) : trav);
     }
 
     /* get the push-traveler */
     getLocPushTraveler(trav, obj)
     {
         /* 
          *   ask our location if we have one; otherwise, just use the
          *   original contained traveler 
          */
         return (location != nil
                 ? location.getLocPushTraveler(trav, obj)
                 : trav);
     }
 
     /*
      *   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;
         
         /* 
          *   Set the actor's posture to the default for the new location.
          *   Do this before effecting the actual travel, so that the
          *   destination can change this default if it wants. 
          */
         gActor.makePosture(location.defaultPosture);
 
         /* protect against 'exit' and the like during the travel attempt */
         try
         {
             /* 
              *   move the actor to our container, traveling entirely within
              *   nested locations 
              */
             gActor.travelWithin(location);
         }
         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)
                 || gActor.posture != defaultPosture)
                 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 the desired
              *   location 
              */
             if (!gActor.isDirectlyIn(location))
                 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,
      *   when we leave a nested room, we end up in the enclosing room.  
      */
     exitDestination = (location)
 
     /*
      *   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.  
      */
     stagingLocations = []
 
     /*
      *   By default, we have no 'exit destination' - that is, it's not
      *   possible to get out of the high nested room.  
      */
     exitDestination = nil
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   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.
      */
     tryMovingIntoNested()
     {
         /* try sitting on me */
         return tryImplicitAction(SitOn, 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);
         }
     }
 
     /* "get on/in" is the same as "sit on" */
     dobjFor(Board) asDobjFor(SitOn)
 
     /* "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]
 
     /*
      *   Try an implied command to move the actor from outside of this
      *   nested room into this nested room.
      */
     tryMovingIntoNested()
     {
         /* try lying on me */
         return tryImplicitAction(LieOn, self);
     }
 
     /* tryMovingIntoNested failure message is "must sit on chair" */
     mustMoveIntoProp = &mustLieOnMsg
 
     /* default posture in this nested room is sitting */
     defaultPosture = lying
 
     /* "get on/in" is the same as "lie on" */
     dobjFor(Board) asDobjFor(LieOn)
 ;
 
 /*
  *   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)
 
     /*
      *   Try an implied command to move the actor from outside of this
      *   nested room into this nested room.
      */
     tryMovingIntoNested()
     {
         /* try standing on me */
         return tryImplicitAction(StandOn, 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);
         }
     }
 
     /* "get off of" is the same as "stand on" */
     dobjFor(Board) asDobjFor(StandOn)
 
     /*
      *   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, TravelVia)
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   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.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);
     }
 ;
 
TADS 3 Library Manual
Generated on 9/15/2006 from TADS version 3.0.12