travel.t
#charset "us-ascii"
/*
* Copyright (c) 2000, 2006 Michael J. Roberts. All Rights Reserved.
*
* TADS 3 Library - travel
*
* This module defines the parts of the simulation model related to
* travel: rooms and other locations, directions, passages.
*/
/* include the library header */
#include "adv3.h"
infiniteLoop()
{
for (local i = 1 ; i < 100 ; ) ;
}
/* ------------------------------------------------------------------------ */
/*
* Directions. Each direction is represented by an instance of
* Direction.
*
* A Direction object encapsulates information about a travel direction.
* By using an object to represent each possible travel direction, we
* make it possible for a game or library extension to add new custom
* directions without having to change the basic library.
*/
class Direction: object
/*
* The link property for the direction. This is a property pointer
* that we use to ask an actor's location for a TravelConnector when
* the actor attempts to travel in this direction.
*/
dirProp = nil
/*
* The default TravelConnector when an actor attempts travel in this
* direction, but the actor's location does not define the dirProp
* property for this direction. This is almost always a connector
* that goes nowhere, such as noTravel, since this is used only when
* a location doesn't have a link for travel in this direction.
*/
defaultConnector(loc) { return noTravel; }
/*
* Initialize. We'll use this routine to add each Direction
* instance to the master direction list (Direction.allDirections)
* during pre-initialization.
*/
initializeDirection()
{
/* add myself to the master direction list */
Direction.allDirections.append(self);
}
/*
* Class initialization - this is called once on the class object.
* We'll build our master list of all of the Direction objects in
* the game, and then sort the list using the sorting order.
*/
initializeDirectionClass()
{
/* initialize each individual Direction object */
forEachInstance(Direction, { dir: dir.initializeDirection() });
/*
* sort the direction list according to the individual Directin
* objects' defined sorting orders
*/
allDirections.sort(SortAsc, {a, b: a.sortingOrder - b.sortingOrder});
}
/*
* Our sorting order in the master list. We use this to present
* directions in a consistent, aesthetically pleasing order in
* listings involving multiple directions. The sorting order is
* simply an integer that gives the relative position in the list;
* the list of directions is sorted from lowest sorting order to
* highest. Sorting order numbers don't have to be contiguous,
* since we simply put the directions in an order that makes the
* sortingOrder values ascend through the list.
*/
sortingOrder = 1
/*
* A master list of the defined directions. We build this
* automatically during initialization to include each Direction
* instance. Any operation that wants to iterate over all possible
* directions can run through this list.
*
* By using this master list to enumerate all possible directions
* rather than a hard-coded set of pre-defined directions, we can
* ensure that any new directions that a game or library extension
* adds will be incorporated automatically into the library simply
* by virtue of the existence Direction instances to represent the
* new directions.
*/
allDirections = static new Vector(10)
;
/*
* The base class for compass directions (north, south, etc). We don't
* add anything to the basic Direction class, but we use a separate
* class for compass directions to allow language-specific
* customizations for all of the directions and to allow travel commands
* to treat these specially when needed.
*/
class CompassDirection: Direction
;
/*
* The base class for shipboard directions (port, aft, etc).
*/
class ShipboardDirection: Direction
defaultConnector(loc)
{
/*
* If 'loc' is aboard a ship, use noTravel as the default
* connector, since we simply can't go this direction. If we're
* not aboard ship, use the special connector noShipTravel,
* which is meant to convey that shipboard directions make no
* sense when not aboard a ship.
*/
return (loc.isShipboard ? noTravel : noShipTravel);
}
;
/*
* The base class for vertical directions (up, down)
*/
class VerticalDirection: Direction
;
/*
* The base class for "relative" directions (in, out)
*/
class RelativeDirection: Direction
;
/*
* Individual compass directions.
*
* Our macro defines a direction object with a name based on the
* direction's room travel link property and the base class. So,
* DefineDirection(north, Compass) defines a direction object called
* 'northDirection' based on the CompassDirection class, with the link
* property 'north' and default travel connector 'noTravel'.
*
* Note that we define a sorting order among the default directions as
* follows. First, we define several groups of related directions,
* which we put in a relative order: the compass directions, then the
* vertical directions, then the "relative" (in/out) directions, and
* finally the shipboard directions. Then, we order the directions
* within these groups. For the sortingOrder values, we use arbitrary
* integers with fairly wide separations, to leave plenty of room for
* custom game-specific directions to be added before, between, after,
* or within these pre-defined groups.
*/
#define DefineDirection(prop, base, order) \
prop##Direction: base##Direction \
dirProp = &prop \
sortingOrder = order
DefineDirection(north, Compass, 1000);
DefineDirection(south, Compass, 1100);
DefineDirection(east, Compass, 1200);
DefineDirection(west, Compass, 1300);
DefineDirection(northeast, Compass, 1400);
DefineDirection(northwest, Compass, 1500);
DefineDirection(southeast, Compass, 1600);
DefineDirection(southwest, Compass, 1700);
DefineDirection(up, Vertical, 3000);
DefineDirection(down, Vertical, 3100);
DefineDirection(in, Relative, 5000)
defaultConnector(loc) { return noTravelIn; }
;
DefineDirection(out, Relative, 5100)
defaultConnector(loc) { return noTravelOut; }
;
DefineDirection(fore, Shipboard, 7000);
DefineDirection(aft, Shipboard, 7100);
DefineDirection(port, Shipboard, 7200);
DefineDirection(starboard, Shipboard, 7300);
/* ------------------------------------------------------------------------ */
/*
* Travel Message Handler. This contains a set of messages that are
* specific to different types of TravelConnector objects, to describe
* NPC arrivals and departures via these connectors when the NPC's are in
* view of the player character.
*
* This base class implements the methods simply by calling the
* corresponding gLibMessages message methods.
*
* The purpose of providing this variety of connector-specific handlers
* is to make it easy for individual travelers to customize the
* arrival/departure messages for specific connector subclasses. These
* messages sometimes benefit from customization for specific
* traveler/connector combinations; the easiest way to enable such
* granular customization is to make it possible to override a message
* per connector type on the traveler object.
*/
class TravelMessageHandler: object
/*
* Get the traveler for the purposes of arrival/departure messages.
* Implementations that aren't themselves the travelers should
* override this to supply the correct nominal traveler.
*/
getNominalTraveler() { return self; }
/* generic arrival/departure - for the base TravelConnector class */
sayArriving(conn) { gLibMessages.sayArriving(getNominalTraveler()); }
sayDeparting(conn) { gLibMessages.sayDeparting(getNominalTraveler()); }
/* generic local arrival and departure messages */
sayArrivingLocally(dest, conn)
{ gLibMessages.sayArrivingLocally(getNominalTraveler(), dest); }
sayDepartingLocally(dest, conn)
{ gLibMessages.sayDepartingLocally(getNominalTraveler(), dest); }
/* generic remote travel message */
sayTravelingRemotely(dest, conn)
{ gLibMessages.sayTravelingRemotely(getNominalTraveler(), dest); }
/* directional arrival/departure - for RoomConnector */
sayArrivingDir(dir, conn) { dir.sayArriving(getNominalTraveler()); }
sayDepartingDir(dir, conn) { dir.sayDeparting(getNominalTraveler()); }
/* arrival/departure via a ThroughPassage */
sayArrivingThroughPassage(conn)
{
gLibMessages.sayArrivingThroughPassage(getNominalTraveler(), conn);
}
sayDepartingThroughPassage(conn)
{
gLibMessages.sayDepartingThroughPassage(getNominalTraveler(), conn);
}
/* arrival/departure via a PathPassage */
sayArrivingViaPath(conn)
{ gLibMessages.sayArrivingViaPath(getNominalTraveler(), conn); }
sayDepartingViaPath(conn)
{ gLibMessages.sayDepartingViaPath(getNominalTraveler(), conn); }
/* arrival/departure up/down stairs */
sayArrivingUpStairs(conn)
{ gLibMessages.sayArrivingUpStairs(getNominalTraveler(), conn); }
sayArrivingDownStairs(conn)
{ gLibMessages.sayArrivingDownStairs(getNominalTraveler(), conn); }
sayDepartingUpStairs(conn)
{ gLibMessages.sayDepartingUpStairs(getNominalTraveler(), conn); }
sayDepartingDownStairs(conn)
{ gLibMessages.sayDepartingDownStairs(getNominalTraveler(), conn); }
;
/* ------------------------------------------------------------------------ */
/*
* A Traveler is an object that can travel. The two main kinds of
* travelers are Actors and Vehicles. A vehicle can contain multiple
* actors, so a single vehicle travel operation could move several
* actors.
*
* This class is intended to be multiply inherited, since it is not based
* on any simulation object class.
*/
class Traveler: TravelMessageHandler
/*
* Check, using pre-condition rules, that the traveler is directly
* in the given room. We'll attempt to implicitly remove the actor
* from any nested rooms within the given room.
*/
checkDirectlyInRoom(dest, allowImplicit)
{
/* ask the destination to ensure the traveler is in it directly */
return dest.checkTravelerDirectlyInRoom(self, allowImplicit);
}
/*
* Check, using pre-condition rules, that the traveler is in the
* given room, moving the traveler to the room if possible.
*/
checkMovingTravelerInto(room, allowImplicit)
{
/* subclasses must override */
}
/*
* Get the travel preconditions specific to the traveler, for the
* given connector. By default, we return no extra conditions.
*/
travelerPreCond(conn) { return []; }
/*
* Can the traveler travel via the given connector to the given
* destination? Returns true if the travel is permitted, nil if not.
*
* By default, this simply returns true to indicate that the travel
* is allowed. Individual instances can override this to enforce
* limitations on what kind of travel the traveler can perform.
*/
canTravelVia(connector, dest) { return true; }
/*
* Explain why the given travel is not possible. This is called when
* canTravelVia() returns nil, to display a report to the player
* explaining why the travel was disallowed.
*
* By default, we do nothing, since our default canTravelVia() never
* disallows any travel. If canTravelVia() is overridden to disallow
* travel under some conditions, this must be overridden to generate
* an appropriate explanatory report.
*/
explainNoTravelVia(connector, dest) { }
/*
* Show my departure message - this is shown when I'm leaving a room
* where the player character can see me for another room where the
* player character cannot see me.
*/
describeDeparture(dest, connector)
{
/*
* If we're visible to the player character, describe the
* departure. Ask the connector to describe the travel, since
* it knows the direction of travel and can describe special
* things like climbing stairs.
*
* Don't bother to describe the departure if the traveler is the
* player character, or the player character is inside the
* traveler. The PC obviously can't leave the presence of
* itself.
*/
if (!isActorTraveling(gPlayerChar))
{
/*
* invoke the departure with a visual sense context for the
* traveler
*/
callWithSenseContext(self, sight,
{: describeNpcDeparture(dest, connector)});
}
}
/*
* Describe the departure of a non-player character, or any traveler
* not involving the player character.
*/
describeNpcDeparture(dest, connector)
{
/*
* If the PC can see our destination, we're doing "local travel"
* - we're traveling from one top-level location to another, but
* entirely in view of the PC. In this case, the PC was already
* aware of our presence, so we don't want to describe the
* departure as though we're actually leaving; we're just moving
* around. If the destination isn't in sight, then we're truly
* departing.
*/
if (gPlayerChar.canSee(dest))
{
/*
* We're moving from somewhere within the PC's sight to
* another location within the PC's sight, so we're not
* actually departing, from the PC's point of view: we're
* just moving around a bit.
*
* - If we're moving further away from the PC - that is, if
* we're moving out of the PC's own top-level location and
* into a different top-level location, describe this as a
* "local departure."
*
* - If we're moving closer to the PC - moving from a top
* level location that doesn't contain the PC to the PC's
* current top-level location - say nothing. We'll leave it
* to the "local arrival" message to mention it, because
* that's the more relevant aspect of the travel from the
* PC's POV: we're approaching the PC.
*
* - If we're moving from one top-level location to another,
* say nothing. We'll leave this to arrival time as well.
* Whether we describe remote-to-remote travel as arrival or
* departure is just arbitrary; we only want to generate one
* message, but there's no general reason to prefer
* generating the message at departure vs arrival.
*/
if (gPlayerChar.isIn(getOutermostRoom()))
{
/*
* we're moving further away from the player - describe
* this as a local departure
*/
connector.describeLocalDeparture(self, location, dest);
}
}
else
{
/* we're really departing - let the connector describe it */
connector.describeDeparture(self, location, dest);
}
}
/*
* Show my arrival message - this is shown when I'm entering a room
* in view of the player character from a location where the player
* character could not see me. 'backConnector' is the connector in
* the destination location from which we appear to be emerging.
*/
describeArrival(origin, backConnector)
{
/*
* If the player character is participating in the travel,
* describe the arrival from the point of view of the player
* character by showing the "look around" description of the new
* location.
*
* Otherwise, describe the NPC traveler's arrival. Only
* describe the arrival if the PC can see the traveler, for
* obvious reasons.
*/
if (isActorTraveling(gPlayerChar))
{
/*
* Add an intra-report separator, to visually separate the
* description of the arrival from any result text that came
* before. (For example, if we performed an implied command
* before we could begin travel, this will visually separate
* the results of the implied command from the arrival
* message.)
*/
say(gLibMessages.intraCommandSeparator);
/*
* The player character is traveling - show the travel from
* the PC's perspective.
*/
gPlayerChar.lookAround(gameMain.verboseMode.isOn);
}
else if (travelerSeenBy(gPlayerChar))
{
/*
* The player character isn't traveling, but the PC can see
* me now that I'm in my new location, so describe the
* arrival of the traveler. Do this within a visual sense
* context for the traveler.
*/
callWithSenseContext(
self, sight, {: describeNpcArrival(origin, backConnector)});
}
}
/*
* Describe the arrival of a non-player character or any other
* traveler that doesn't involve the player character.
*/
describeNpcArrival(origin, backConnector)
{
/*
* If we know the connector back, use its arrival message;
* otherwise, use a generic arrival message.
*/
if (backConnector != nil)
{
/*
* If the PC can see the origin, we're not actually arriving
* anew; instead, we're simply moving around within the PC's
* field of view. In these cases, we don't want to use the
* normal arrival message, because we're not truly arriving;
* we could be moving closer to the player, further away from
* the player, or moving laterally from one remote location
* to another. If the PC can't see the origin, we're truly
* arriving.
*/
if (gPlayerChar.canSee(origin))
{
/*
* We're not arriving anew, but just moving around within
* the PC's field of view.
*
* - If we're moving closer to the PC - moving from a
* top-level location that doesn't contain the PC to one
* that does - describe this as a "local arrival."
*
* - If we're moving further away from the PC - from
* within the PC's top-level location to a different
* top-level location - don't say anything. We will have
* already described this as a "local departure" during
* the departure message phase, and from the PC's point
* of view, that fully covers it.
*
* - If we're moving laterally - from one remote
* top-level location to another ("remote" means "does
* not contain the PC") - it's arbitrary whether this
* should be described at arrival or departure time. We
* arbitrarily pick arrival, so describe the lateral
* local travel now.
*/
if (gPlayerChar.isIn(getOutermostRoom()))
{
/*
* We're now in the same top-level location as the
* PC, so we've moved closer to the PC, so describe
* this as a "local arrival."
*/
backConnector.describeLocalArrival(
self, origin, location);
}
else if (!gPlayerChar.isIn(origin))
{
/*
* We're *not* in the same top-level location as the
* PC, and we weren't on departure either, so this is
* a lateral move - a move from one remote location
* to another. Describe it as such now.
*/
backConnector.describeRemoteTravel(
self, origin, location);
}
}
else
{
/* we're arriving anew - let the connector describe it */
backConnector.describeArrival(self, origin, location);
}
}
else
{
/* there's no back-connector, so use a generic arrival message */
gLibMessages.sayArriving(self);
}
}
/*
* Travel to a new location. Moves the traveler to a new location
* via the given connector, triggering any side effects of the
* travel.
*
* Note that this routine is not normally called directly; in most
* cases, the actor's travelTo is called, and it in turn invokes
* this method in the appropriate traveler.
*
* 'dest' is the new location to which we're traveling. 'connector'
* is the TravelConnector we're traversing from the source location
* to reach the new destination; the connector is normally the
* invoker of this routine. 'backConnector' is the connector in the
* destination from which the actor will appear to emerge on the
* other end of the travel.
*/
travelerTravelTo(dest, connector, backConnector)
{
local origin;
local isDescribed;
local actors;
/*
* Remember my departure original location - this is the
* location where the traveler was before we moved.
*/
origin = location;
/*
* Determine if we're describing the travel. We'll describe it
* if we're actually changing locations, or if the connector is
* explicitly a "circular" passage, which means that it connects
* back to the same location but involves a non-trivial passage,
* as sometimes happens in settings like caves.
*/
isDescribed = (dest != origin || connector.isCircularPassage);
/*
* Send a before-departure notification to everyone connected to
* by containment to the traveler.
*/
getNotifyTable().forEachAssoc(
{obj, val: obj.beforeTravel(self, connector)});
/* notify the actor initiating the travel */
if (gActor != nil)
gActor.actorTravel(self, connector);
/* tell the old room we're leaving, if we changed locations */
if (origin != nil && isDescribed)
origin.travelerLeaving(self, dest, connector);
/* notify the connector that we're traversing it */
connector.noteTraversal(self);
/* get the list of traveling actors */
actors = getTravelerActors();
/* move to the destination */
moveIntoForTravel(dest);
/*
* We've successfully completed the travel, so remember it for
* each actor involved in the travel. Do this only if we
* actually went somewhere, or if the connector is circular - if
* we didn't end up going anywhere, and the connector isn't
* circular, we must have turned back halfway without completing
* the travel.
*/
if (dest != origin || connector.rememberCircularPassage)
{
foreach (local cur in actors)
{
/* ask the actor to remember the travel */
cur.rememberTravel(origin, dest, backConnector);
/* remember the destination of the connector for this actor */
connector.rememberTravel(origin, cur, dest);
/*
* If there's a back-connector, also remember travel in
* the other direction. The actor can reasonably be
* expected to assume that the connector through which
* it's arriving connects back to the source location, so
* remember the association.
*/
if (backConnector != nil)
backConnector.rememberTravel(dest, cur, origin);
}
}
/*
* Recalculate the global sense context for message generation
* purposes, since we've moved to a new location. It's possible
* that we've arrived at the player character's location, in
* which case we might have been suppressing messages (due to
* our being in a different room) but should now show messages
* (because we're now near the player character).
*/
if (gAction != nil)
gAction.recalcSenseContext();
/* tell the new room we're arriving, if we changed locations */
if (location != nil && isDescribed)
location.travelerArriving(self, origin, connector, backConnector);
/* notify objects now connected by containment of the arrival */
getNotifyTable().forEachAssoc(
{obj, val: obj.afterTravel(self, connector)});
}
/*
* Perform "local" travel - that is, travel between nested rooms
* within a single top-level location. By default, we simply defer
* to the actor to let it perform the local travel.
*/
travelerTravelWithin(actor, dest)
{
actor.travelerTravelWithin(actor, dest);
}
/*
* Get a lookup table giving the set of objects to be notified of a
* beforeTravel/afterTravel event. By default, we return a table
* including every object connected to the traveler by containment.
*/
getNotifyTable()
{
/* return the table of objects connected by containment */
return connectionTable();
}
/*
* Determine if the given actor can see this traveler. By default,
* we'll simply check to see if the actor can see 'self'.
*/
travelerSeenBy(actor) { return actor.canSee(self); }
/*
* Is the given actor traveling with this traveler? Returns true if
* the actor is in my getTravelerActors list.
*/
isActorTraveling(actor)
{
/* check to see if the given actor is in my actor list */
return getTravelerActors.indexOf(actor) != nil;
}
/*
* Is the given object being carried by the traveler? Returns true
* if the object is inside the traveler itself, or is inside any of
* the actors traveling.
*/
isTravelerCarrying(obj)
{
/* if the object is inside the traveler, it's being carried */
if (obj.isIn(self))
return true;
/* if the object is inside any traveling actor, it's being carried */
foreach (local cur in getTravelerActors)
{
if (obj.isIn(cur))
return true;
}
/* the object isn't being carried */
return nil;
}
/* invoke a callback function for each traveling actor */
forEachTravelingActor(func)
{
/* by default, get the list, and invoke the callback per item */
getTravelerActors.forEach(func);
}
/*
* Get the list of actors taking part in the travel. When an actor
* is the traveler, this list simply contains the actor itself; for
* a vehicle or other composite traveler that moves more than one
* actor at a time, this should return the list of all of the actors
* involved in the travel.
*/
getTravelerActors = []
/*
* Get the list of actors traveling undo their own power. In the
* case of an actor traveling directly, this is just the actor; in
* the case of an actor pushing something, this is likewise the
* actor; in the case of a group of actors traveling together, this
* is the list of traveling actors; in the case of a vehicle, this
* is an empty list, since anyone traveling with the vehicle is
* traveling under the vehicle's power.
*/
getTravelerMotiveActors = []
;
/* ------------------------------------------------------------------------ */
/*
* A Travel Connector is a special connection interface that allows for
* travel from one location to another. Most actor movement, except for
* movement between locations related by containment (such as from a room
* to sitting in a chair within the room) are handled through travel
* connector objects.
*
* Travel connectors are used in the directional link properties in rooms
* - north, south, east, west, in, out, etc. A room direction link
* property is always set to a travel connector - but note that a room is
* itself a travel connector, so a travel link in one room can simply be
* set to point directly to another room. In many cases, rooms
* themselves serve as travel connectors, so that one room can point a
* direction link property directly to another room.
*
* Some travel connectors are physical objects in the simulation, such as
* doors or stairways; other connectors are just abstract objects that
* represent connections, but don't appear as manipulable objects in the
* game.
*
* A travel connector provides several types of information about travel
* through its connection:
*
* - For actors actually traveling, the connector provides a method that
* moves an actor through the connector. This method can trigger any
* side effects of the travel.
*
* - For automatic map builders, actor scripts, and other callers who
* want to learn what can be known about the link without actually
* traversing it, the connector provides an "apparent destination"
* method. This method returns the destination of travel through the
* connector that a given actor would expect just by looking at the
* connector. The important thing about this routine is that it doesn't
* trigger any side effects, but simply indicates whether travel is
* apparently possible, and if so what the destination of the travel
* would be.
*/
class TravelConnector: Thing
/*
* Get any connector-specific pre-conditions for travel via this
* connector.
*/
connectorTravelPreCond()
{
local lst;
/* start with no conditions */
lst = [];
/* if we have a staging location, require that we're in it */
if (connectorStagingLocation != nil)
lst = [new TravelerDirectlyInRoom(gActor, self,
connectorStagingLocation)];
/*
* If we're a physical Thing with a non-nil location, require
* that we be touchable. Only physical objects need to be
* touchable; connectors are sometimes abstract objects, which
* obviously can't be touched.
*/
if (ofKind(Thing) && location != nil)
{
/* require that the traveler can touch the connector */
local cond = new TouchObjCondition(gActor.getTraveler(self));
lst += new ObjectPreCondition(self, cond);
}
/* return the result */
return lst;
}
/*
* The "staging location" for travel through this connector. By
* default, if we have a location, that's our staging location; if
* we don't have a location (in which case we probably an outermost
* room), we don't have a staging location.
*/
connectorStagingLocation = (location)
/*
* Get the travel preconditions that this connector requires for
* travel by the given actor. In most cases, this won't depend on
* the actor, but it's provided as a parameter anyway; in most cases,
* this will just apply the conditions that are relevant to actors as
* travelers.
*
* By default, we require actors to be "travel ready" before
* traversing a connector. The exact meaning of "travel ready" is
* provided by the actor's immediate location, but it usually simply
* means that the actor is standing. This ensures that the actor
* isn't sitting in a chair or lying down or something like that.
* Some connectors might not require this, so this routine can be
* overridden per connector.
*
* Note that this will only be called when an actor is the traveler.
* When a vehicle or other kind of traveler is doing the travel, this
* will not be invoked.
*/
actorTravelPreCond(actor)
{
/*
* create an object precondition ensuring that the actor is
* "travel ready"; the object of this precondition is the
* connector itself
*/
return [new ObjectPreCondition(self, actorTravelReady)];
}
/*
* Barrier or barriers to travel. This property can be set to a
* single TravelBarrier object or to a list of TravelBarrier
* objects. checkTravelBarriers() checks each barrier specified
* here.
*/
travelBarrier = []
/*
* Check barriers. The TravelVia check() routine must call this to
* enforce barriers.
*/
checkTravelBarriers(dest)
{
local traveler;
local lst;
/* get the traveler */
traveler = gActor.getTraveler(self);
/* ask the traveler what it thinks of travel through this connector */
if (!traveler.canTravelVia(self, dest))
{
/* explain why the traveler can't pass */
traveler.explainNoTravelVia(self, dest);
/* terminate the command */
exit;
}
/* check any travel conditions we apply directly */
if (!canTravelerPass(traveler))
{
/* explain why the traveler can't pass */
explainTravelBarrier(traveler);
/* terminate the command */
exit;
}
/* get the barrier list */
lst = travelBarrier;
/* if it's just a single object, make it a list of one element */
if (!lst.ofKind(Collection))
lst = [lst];
/* check each item in our barrier list */
foreach (local cur in lst)
{
/* if this barrier doesn't allow travel, we cannot travel */
if (!cur.canTravelerPass(traveler))
{
/* ask the barrier to explain why travel isn't possible */
cur.explainTravelBarrier(traveler);
/* terminate the command */
exit;
}
}
}
/*
* Check to see if the Traveler object is allowed to travel through
* this connector. Returns true if travel is allowed, nil if not.
*
* This is called from checkTravelBarriers() to check any conditions
* coded directly into the TravelConnector. By default, we simply
* return true; subclasses can override this to apply special
* conditions.
*
* If an override wants to disallow travel, it should return nil
* here, and then provide an override for explainTravelBarrier() to
* provide a descriptive message explaining why the travel isn't
* allowed.
*
* Conditions here serve essentially the same purpose as barrier
* conditions. The purpose of providing this additional place for
* the same type of conditions is simply to improve the convenience
* of defining travel conditions for cases where barriers are
* unnecessary. The main benefit of using a barrier is that the same
* barrier object can be re-used with multiple connectors, so if the
* same set of travel conditions apply to several different
* connectors, barriers allow the logic to be defined once in a
* single barrier object and then re-used easily in each place it's
* needed. However, when a particular condition is needed in only
* one place, creating a barrier to represent the condition is a bit
* verbose; in such cases, the condition can be placed in this method
* more conveniently.
*/
canTravelerPass(traveler) { return true; }
/*
* Explain why canTravelerPass() returned nil. This is called to
* display an explanation of why travel is not allowed by
* self.canTravelerPass().
*
* Since the default canTravelerPass() always allows travel, the
* default implementation of this method does nothing. Whenever
* canTravelerPass() is overridden to return nil, this should also be
* overridden to provide an appropriate explanation.
*/
explainTravelBarrier(traveler) { }
/*
* Is this connector listed? This indicates whether or not the exit
* is allowed to be displayed in lists of exits, such as in the
* status line or in "you can't go that way" messages. By default,
* all exits are allowed to appear in listings.
*
* Note that this indicates if listing is ALLOWED - it doesn't
* guarantee that listing actually occurs. A connector can be
* listed only if this is true, AND the point-of-view actor for the
* listing can perceive the exit (which means that
* isConnectorApparent must return true, and there must be
* sufficient light to see the exit).
*/
isConnectorListed = true
/*
* Get an unlisted proxy for this connector. This is normally
* called from the asExit() macro to set up one room exit direction
* as an unlisted synonym for another.
*/
createUnlistedProxy() { return new UnlistedProxyConnector(self); }
/*
* Determine if the travel connection is apparent - as a travel
* connector - to the actor in the given origin location. This
* doesn't indicate whether or not travel is possible, or where
* travel goes, or that the actor can tell where the passage goes;
* this merely indicates whether or not the actor should realize
* that the passage exists at all.
*
* A closed door, for example, would return true, because even a
* closed door makes it clear that travel is possible in the
* direction, even if it's not possible currently. A secret door,
* on the other hand, would return nil while closed, because it
* would not be apparent to the actor that the object is a door at
* all.
*/
isConnectorApparent(origin, actor)
{
/* by default, passages are apparent */
return true;
}
/*
* Determine if the travel connection is passable by the given
* traveler in the current state. For example, a door would return
* true when open, nil when closed.
*
* This information is intended to help game code probing the
* structure of the map. This information is NOT used in actor
* travel; for actor travel, we rely on custom checks in the
* connector's TravelVia handler to enforce the conditions of travel.
* Actor travel uses TravelVia customizations rather than this method
* because that allows better specificity in reporting failures.
* This method lets game code get at the same information, but in a
* more coarse-grained fashion.
*/
isConnectorPassable(origin, traveler)
{
/* by default, we're passable */
return true;
}
/*
* Get the apparent destination of travel by the actor to the given
* origin. This returns the location to which the connector
* travels, AS FAR AS THE ACTOR KNOWS. If the actor does not know
* and cannot tell where the connector leads, this should return nil.
*
* Note that this method does NOT necessarily return the actual
* destination, because we obviously can't know the destination for
* certain until we traverse the connection. Rather, the point of
* this routine is to return as much information as the actor is
* supposed to have. This can be used for purposes like
* auto-mapping, where we'd want to show what the player character
* knows of the map, and NPC goal-seeking, where an NPC tries to
* figure out how to get from one point to another based on the
* NPC's knowledge of the map. In these sorts of applications, it's
* important to use only knowledge that the actor is supposed to
* have within the parameters of the simulation.
*
* Callers should always test isConnectorApparent() before calling
* this routine. This routine does not check to ensure that the
* connector is apparent, so it could return misleading information
* if used independently of isConnectorApparent(); for example, if
* the connector *formerly* worked but has now disappeared, and the
* actor has a memory of the former destination, we'll return the
* remembered destination.
*
* The actor can know the destination by a number of means:
*
* 1. The location is familiar to the character. For example, if
* the setting is the character's own house, the character would
* obviously know the house well, so would know where you'd end up
* going east from the living room or south from the kitchen. We
* use the origin method actorKnowsDestination() to determine this.
*
* 2. The destination is readily visible from the origin location,
* or is clearly marked. For example, in an outdoor setting, it
* might be clear that going east from the field takes you to the
* hilltop. In an indoor setting, an open passage might make it
* clear that going east from the living room takes you to the
* dining room. We use the origin method actorKnowsDestination() to
* determine this.
*
* 3. The actor has been through the connector already in the course
* of the game, and so remembers the connection by virtue of recent
* experience. If our travelMemory class property is set to a
* non-nil lookup table object, then we'll automatically use the
* lookup table to remember the destination each time an actor
* travels via a connector, and use this information by default to
* provide apparent destination information.
*/
getApparentDestination(origin, actor)
{
local dest;
/*
* Ask the origin if the actor knows the destination for the
* given connector. If so, and we can determine our
* destination, then return the destination.
*/
if (origin.actorKnowsDestination(actor, self)
&& (dest = getDestination(origin,
actor.getTraveler(self))) != nil)
return dest;
/*
* If we have a travelMemory table, look to see if the traversal
* of this actor via this connector from this origin is recorded
* in the table, and if so, assume that the destination is the
* same as it was last time.
*
* Note that we ignore our memory of travel if we never saw the
* destination of the travel (which would be the case if the
* destination was dark every time we've been there, so we've
* never seen any details about the location).
*/
if (travelMemory != nil
&& (dest = travelMemory[[actor, origin, self]]) != nil
&& actor.hasSeen(dest))
{
/* we know the destination from past experience */
return dest;
}
/* we don't know the destination */
return nil;
}
/*
* Get our destination, given the traveler and the origin location.
*
* This method is required to return the current destination for the
* travel. If the connector doesn't go anywhere, this should return
* nil. The results of this method must be stable for the extent of
* a turn, up until the time travel actually occurs; in other words,
* it must be possible to call this routine simply for information
* purposes, to determine where the travel will end up.
*
* This method should not trigger any side effects, since it's
* necessary to be able to call this method more than once in the
* course of a given travel command. If it's necessary to trigger
* side effects when the connector is actually traversed, apply the
* side effects in noteTraversal().
*
* For auto-mapping and the like, note that getApparentDestination()
* is a better choice, since this method has internal information
* that might not be apparent to the characters in the game and thus
* shouldn't be revealed through something like an auto-map. This
* method is intended for internal use in the course of processing a
* travel action, since it knows the true destination of the travel.
*/
getDestination(origin, traveler) { return nil; }
/*
* Get the travel connector leading to the given destination from the
* given origin and for the given travel. Return nil if we don't
* know a connector leading there.
*
* By default, we simply return 'self' if our destination is the
* given destination, or nil if not.
*
* Some subclasses might encapsulate one or more "secondary"
* connectors - that is, the main connector might choose among
* multiple other connectors. In these cases, the secondary
* connectors typically won't be linked to directions on their own,
* so the room can't see them directly - it can only find them
* through us, since we're effectively a wrapper for the secondary
* connectors. In these cases, we won't have any single destination
* ourself, so getDestination() will have to return nil. But we
* *can* work backwards: given a destination, we can find the
* secondary connector that points to that destination. That's what
* this routine is for.
*/
connectorGetConnectorTo(origin, traveler, dest)
{
/* if we go there, return 'self', else return nil */
return (getDestination(origin, traveler) == dest ? self : nil);
}
/*
* Note that the connector is being traversed. This is invoked just
* before the traveler is moved; this notification is fired after the
* other travel-related notifications (beforeTravel, actorTravel,
* travelerLeaving). This is a good place to display any special
* messages describing what happens during the travel, because any
* messages displayed here will come after any messages related to
* reactions from other objects.
*/
noteTraversal(traveler)
{
/* do nothing by default */
}
/*
* Service routine: add a memory of a successful traversal of a
* travel connector. If we have a travel memory table, we'll add
* the traversal to the table, so that we can find it later.
*
* This is called from Traveler.travelerTravelTo() on successful
* travel. We're called for each actor participating in the travel.
*/
rememberTravel(origin, actor, dest)
{
/*
* If we have a travelMemory table, add this traversal. Store
* the destination, keyed by the combination of the actor,
* origin, and connector object (i.e., self) - this will allow
* us to remember the destination we reached last time if we
* want to know where the same route goes in the future.
*/
if (TravelConnector.travelMemory != nil)
TravelConnector.travelMemory[[actor, origin, self]] = dest;
}
/*
* Our "travel memory" table. If this contains a non-nil lookup
* table object, we'll store a record of each successful traversal
* of a travel connector here - we'll record the destination keyed
* by the combination of actor, origin, and connector, so that we
* can later check to see if the actor has any memory of where a
* given connector goes from a given origin.
*
* We keep this information by default, which is why we statically
* create the table here. Keeping this information does involve
* some overhead, so some authors might want to get rid of this
* table (by setting the property to nil) if the game doesn't make
* any use of the information. Note that this table is stored just
* once, in the TravelConnector class itself - there's not a
* separate table per connector.
*/
travelMemory = static new LookupTable(256, 512)
/*
* Is this a "circular" passage? A circular passage is one that
* explicitly connects back to its origin, so that traveling through
* the connector leaves us where we started. When a passage is
* marked as circular, we'll describe travel through the passage
* exactly as though we had actually gone somewhere. By default, if
* traveling through a passage leaves us where we started, we assume
* that nothing happened, so we don't describe any travel.
*
* Circular passages don't often occur in ordinary settings; these
* are mostly useful in disorienting environments, such as twisty
* cave networks, where a passage between locations can change
* direction and even loop back on itself.
*/
isCircularPassage = nil
/*
* Should we remember a circular trip through this passage? By
* default, we remember the destination of a passage that takes us
* back to our origin only if we're explicitly marked as a circular
* passage; in other cases, we assume that the travel was blocked
* somehow instead.
*/
rememberCircularPassage = (isCircularPassage)
/*
* Describe an actor's departure through the connector from the
* given origin to the given destination. This description is from
* the point of view of another actor in the origin location.
*/
describeDeparture(traveler, origin, dest)
{
local dir;
/*
* See if we can find a direction linked to this connector from
* the origin location. If so, describe the departure using the
* direction; otherwise, describe it using the generic departure
* message.
*
* Find the connector from the player character's perspective,
* because the description we're generating is for the player's
* benefit and thus should be from the PC's perspective.
*/
if ((dir = origin.directionForConnector(self, gPlayerChar)) != nil)
{
/*
* We found a direction linked to the connector, so this
* must have been the way they traveled. Describe the
* departure as being in that direction.
*
* Note that it's possible that more than one direction is
* linked to the same connector. In such cases, we'll just
* take the first link we find, because it's equally
* accurate to say that the actor went in any of the
* directions linked to the same connector.
*/
traveler.sayDepartingDir(dir, self);
}
else
{
/*
* We didn't find any direction out of the origin linking to
* this connector, so we don't know how what direction they
* went. Show the generic departure message.
*/
traveler.sayDeparting(self);
}
}
/*
* Describe an actor's arrival through the connector from the given
* origin into the given destination. This description is from the
* point of view of another actor in the destination.
*
* Note that this is called on the connector that reverses the
* travel, NOT on the connector the actor is actually traversing -
* that is, 'self' is the backwards connector, leading from the
* destination back to the origin location. So, if we have two
* sides to a door, and the actor traverses the first side, this
* will be called on the second side - the one that links the
* destination back to the origin.
*/
describeArrival(traveler, origin, dest)
{
local dir;
/*
* See if we can find a direction linked to this connector in
* the destination location. If so, describe the arrival using
* the direction; otherwise, describe it using a generic arrival
* message.
*
* Find the connector from the player character's perspective,
* because the description we're generating is for the player's
* benefit and thus should be from the PC's perspective.
*/
if ((dir = dest.directionForConnector(self, gPlayerChar)) != nil)
{
/*
* we found a direction linked to this back connector, so
* describe the arrival as coming from the direction we
* found
*/
traveler.sayArrivingDir(dir, self);
}
else
{
/*
* we didn't find any direction links, so use a generic
* arrival message
*/
traveler.sayArriving(self);
}
}
/*
* Describe a "local departure" via this connector. This is called
* when a traveler moves around entirely within the field of view of
* the player character, and move *further away* from the PC - that
* is, the traveler's destination is visible to the PC when we're
* leaving our origin, AND the origin's top-level location contains
* the PC. We'll describe the travel not in terms of truly
* departing, but simply in terms of moving away.
*/
describeLocalDeparture(traveler, origin, dest)
{
/* say that we're departing locally */
traveler.sayDepartingLocally(dest, self);
}
/*
* Describe a "local arrival" via this connector. This is called
* when the traveler moves around entirely within the field of view
* of the player character, and comes *closer* to the PC - that is,
* the traveler's origin is visible to the player character when we
* arrive in our destination, AND the destination's top-level
* location contains the PC. We'll describe the travel not in terms
* of truly arriving, since the traveler was already here to start
* with, but rather as entering the destination, but just in terms of
* moving closer.
*/
describeLocalArrival(traveler, origin, dest)
{
/* say that we're arriving locally */
traveler.sayArrivingLocally(dest, self);
}
/*
* Describe "remote travel" via this connector. This is called when
* the traveler moves around entirely within the field of view of the
* PC, but between two "remote" top-level locations - "remote" means
* "does not contain the PC." In this case, the traveler isn't
* arriving or departing, exactly; it's just moving laterally from
* one top-level location to another.
*/
describeRemoteTravel(traveler, origin, dest)
{
/* say that we're traveling laterally */
traveler.sayTravelingRemotely(dest, self);
}
/*
* Find a connector in the destination location that connects back as
* the source of travel from the given connector when traversed from
* the source location. Returns nil if there is no such connector.
* This must be called while the traveler is still in the source
* location; we'll attempt to find the connector back to the
* traveler's current location.
*
* The purpose of this routine is to identify the connector by which
* the traveler arrives in the new location. This can be used, for
* example, to generate a connector-specific message describing the
* traveler's emergence from the connector (so we can say one thing
* if the traveler arrives via a door, and another if the traveler
* arrives by climing up a ladder).
*
* By default, we'll try to find a travel link in the destination
* that links us back to this same connector, in which case we'll
* return 'self' as the connector from which the traveler emerges in
* the new location. Failing that, we'll look for a travel link
* whose apparent source is the origin location.
*
* This should be overridden for any connector with an explicit
* complementary connector. For example, it is common to implement a
* door using a pair of objects, one representing each side of the
* door; in such cases, each door object would simply return its twin
* here. Note that a complementary connector doesn't actually have
* to go anywhere, since it's still useful to have a connector back
* simply for describing travelers arriving on the connector.
*
* This *must* be overridden when the destination location doesn't
* have a simple connector whose apparent source is this connector,
* because in such cases we won't be able to find the reverse
* connector with our direction search.
*/
connectorBack(traveler, dest)
{
local origin;
/* if there's no destination, there's obviously no connector */
if (dest == nil)
return nil;
/*
* get the origin location - this the traveler's current
* immediate container
*/
origin = traveler.location;
/*
* First, try to find a link back to this same connector - this
* will handle simple symmetrical links with the same connector
* object shared between two rooms.
*
* We try to find the actual connector before looking for a
* connector back to the origin - it's possible that there are
* several ways back to the starting point, and we want to make
* sure we pick the one that was actually traversed if possible.
*/
foreach (local dir in Direction.allDirections)
{
/*
* If this direction link from the destination is linked back
* to this same connector, we have a symmetrical connection,
* so we're the connector back. Note that we're interested
* only in map structure here, so we don't pass an actor; the
* actor isn't actually in the new location, so what the
* actor can see is irrelevant to us here.
*/
if (dest.getTravelConnector(dir, nil) == self)
{
/* the same connector goes in both directions */
return self;
}
}
/*
* we didn't find a link back to the same connector, so try to
* find a link from the destination whose apparent source is the
* origin
*/
foreach (local dir in Direction.allDirections)
{
local conn;
/*
* if this link from the destination has an apparent source
* of our origin, the traveler appears to be arriving from
* this link
*/
if ((conn = dest.getTravelConnector(dir, nil)) != nil
&& conn.fixedSource(dest, traveler) == origin)
{
/*
* this direction has an apparent source of the origin
* room - it's not necessarily the same link they
* traversed, but at least it appears to come from the
* same place they came from, so it'll have to do
*/
return conn;
}
}
/* we couldn't find any link back to the origin */
return nil;
}
/*
* Get the "fixed" source for travelers emerging from this connector,
* if possible. This can return nil if the connector does not have a
* fixed relationship with another connector.
*
* The purpose of this routine is to find complementary connectors
* for simple static map connections. This is especially useful for
* direct room-to-room connections.
*
* When a connector relationship other than a simple static mapping
* exists, the connectors must generally override connectorBack(), in
* which case this routine will not be needed (at least, this routine
* won't be needed as long as the overridden connectorBack() doesn't
* call it). Whenever it is not clear how to implement this routine,
* don't - implement connectorBack() instead.
*/
fixedSource(dest, traveler)
{
/* by default, return nothing */
return nil;
}
/*
* Can the given actor see this connector in the dark, looking from
* the given origin? Returns true if so, nil if not.
*
* This is used to determine if the actor can travel from the given
* origin via this connector when the actor (in the origin location)
* is in darkness.
*
* By default, we implement the usual convention, which is that
* travel from a dark room is possible only when the destination is
* lit. If we can't determine our destination, we will assume that
* the connector is not visible.
*/
isConnectorVisibleInDark(origin, actor)
{
local dest;
/*
* Get my destination - if we can't determine our destination,
* then assume we're not visible.
*/
if ((dest = getDestination(origin, actor.getTraveler(self))) == nil)
return nil;
/*
* Check the ambient illumination level in the destination. If
* it's 2 or higher, then it's lit; otherwise, it's dark. If
* the destination is lit, consider the connector to be visible,
* on the theory that the connector lets a little bit of the
* light from the destination leak into the origin room - just
* enough to make the connection itself visible without making
* anything else in the origin room visible.
*/
return (dest.wouldBeLitFor(actor));
}
/*
* Handle travel in the dark. Specifically, this is called when an
* actor attempts travel from one dark location to another dark
* location. (We don't invoke this in any other case:
* light-to-light, light-to-dark, and dark-to-light travel are all
* allowed without any special checks.)
*
* By default, we will prohibit dark-to-dark travel by calling the
* location's darkTravel handler. Individual connectors can
* override this to allow such travel or apply different handling.
*/
darkTravel(actor, dest)
{
/*
* by default, simply call the actor's location's darkTravel
* handler
*/
actor.location.roomDarkTravel(actor);
}
/*
* Action handler for the internal "TravelVia" action. This is not a
* real action, but is instead a pseudo-action that we implement
* generically for travel via the connector. Subclasses that want to
* handle real actions by traveling via the connector can use
* remapTo(TravelVia) to implement the real action handlers. Note
* that remapTo should be used (rather than, say, asDobjFor), since
* this will ensure that every type of travel through the connector
* actually looks like a TravelVia action, which is useful for
* intercepting travel actions generically in other code.
*/
dobjFor(TravelVia)
{
preCond()
{
/*
* For our preconditions, use the traveler's preconditions,
* plus the location's preconditions, plus any special
* connector-specific preconditions we supply.
*/
return gActor.getTraveler(self).travelerPreCond(self)
+ gActor.location.roomTravelPreCond()
+ connectorTravelPreCond();
}
verify()
{
/*
* Verify travel for the current command's actor through this
* connector. This performs normal action verify processing.
*
* The main purpose of this routine is to allow the connector
* to flag obviously dangerous travel to allow a caller to
* avoid such travel as an implicit or scripted action. In
* most cases, there's no need to make travel illogical
* because there's generally no potential ambiguity involved
* in analyzing a travel verb.
*
* Note that this routine must check with the actor to
* determine if the actor or a vehicle will actually be
* performing the travel, by calling gActor.getTraveler(), if
* the routine cares about the difference.
*/
}
check()
{
local t = gActor.getTraveler(self);
local dest;
/*
* Check the travel.
*
* This routine should take into account the light levels at
* the source and destination locations, if travel between
* dark rooms is to be disallowed.
*/
/* get my destination */
dest = getDestination(t.location, t);
/* check dark-to-dark travel */
gActor.checkDarkTravel(dest, self);
/* enforce barriers */
checkTravelBarriers(dest);
}
action()
{
local t = gActor.getTraveler(self);
local dest;
/*
* Execute the travel, moving the command's actor through the
* travel connection: we carry out any side effects of the
* travel and deliver the actor (if appropriate) to the
* destination of the connector.
*
* Note that this routine must check with the actor to
* determine if the actor or a vehicle will actually be
* performing the travel, by calling gActor.getTraveler(), if
* the routine cares about the difference. In most cases,
* the routine won't care: most implementations of this
* routine will (if they effect any travel at all) eventually
* call gActor.travelTo() to carry out the travel, and that
* routine will always route the actual movement to the
* vehicle if necessary.
*/
/* get my destination */
dest = getDestination(t.location, t);
/* travel to my destination */
gActor.travelTo(dest, self, connectorBack(t, dest));
}
}
;
/*
* A "probe" object, for testing light levels in rooms. This is a dummy
* object that we use for what-if testing - it's not actually part of
* the simulation.
*/
lightProbe: Thing;
/*
* A TravelBarrier can be attached to a TravelConnector, via the
* travelBarrier property, to form a conditional barrier to travel.
*/
class TravelBarrier: object
/*
* Determine if this barrier blocks the given traveler. By default,
* we don't block anyone. This doesn't make us much of a barrier, so
* subclasses should override this with a more specific condition.
*/
canTravelerPass(traveler) { return true; }
/*
* Explain why travel isn't allowed. This should generate an
* appropriate failure report explaining the problem. This is
* invoked when travel is attempted and canTravelerPass returns nil.
* Subclasses must override this.
*/
explainTravelBarrier(traveler) { }
;
/*
* An "unlisted proxy" connector acts as a proxy for another connector.
* We act exactly like the underlying connector, except that we suppress
* the connector from automatic exit lists. This can be used for cases
* where an otherwise normal connector is needed but the connector is
* not to appear in automatic exit lists (such as the status line).
*
* The most common situation where this kind of connector is useful is
* where multiple directions in a given room all go to the same
* destination. In these cases, it's often desirable for some of the
* directions to be unlisted alternatives. The asExit() macro can be
* used for convenience to set up these direction synonyms.
*/
class UnlistedProxyConnector: object
construct(pri)
{
/* remember my underlying primary connector */
primaryConn = pri;
}
/*
* Our underlying connector. Start out with a default TadsObject
* rather than nil in case anyone wants to call a property or test
* inheritance before we're finished with our constructor - this will
* produce reasonable default behavior without having to test for nil
* everywhere.
*/
primaryConn = TadsObject
/* we're not listed */
isConnectorListed = nil
/* map any TravelVia action to our underlying connector */
dobjFor(TravelVia) remapTo(TravelVia, primaryConn)
/* redirect everything we don't handle to the underlying connector */
propNotDefined(prop, [args]) { return primaryConn.(prop)(args...); }
/*
* As a proxy, we don't want to disguise the fact that we're a proxy,
* if someone specifically asks, so admist to being of our own true
* kind; but we also act mostly like our underlying connector, so if
* someone wants to know if we're one of those, say yes to that as
* well. So, return true if the inherited version returns true, and
* also return true if our primary connector would return true.
*/
ofKind(cls) { return inherited(cls) || primaryConn.ofKind(cls); }
;
/*
* A travel connector that doesn't allow any travel - if travel is
* attempted, we simply use the origin's cannotTravel method to display
* an appropriate message.
*/
noTravel: TravelConnector
/* it is obvious that is no passage this way */
isConnectorApparent(origin, actor) { return nil; }
/* this is not a passable connector */
isConnectorPassable(origin, traveler) { return nil; }
dobjFor(TravelVia)
{
/*
* we know that no travel will occur, so we don't need to satisfy
* any preconditions
*/
preCond = []
action()
{
/* we can't go this way - use the origin's default message */
gActor.location.cannotTravel();
}
}
;
/*
* An "ask which" travel connector. Rather than just traversing a
* connector, we ask for a direct object for a specified travel verb; if
* the player supplies the missing indirect object (or if the parser can
* automatically choose a default), we'll perform the travel verb using
* that direct object.
*
* This type of connector has two uses.
*
* First, the library various instances, with appropriate specified
* travel verbs, as the default connector for certain directions that
* frequently end up mapping to in-scenario objects. Specifically,
* noTravelIn, noTravelDown, and noTravelOut are used as the default in,
* down, and out connectors. If the player types DOWN, for example, and
* there's no override for 'down' in a given room, then we'll invoke
* noTravelDown; this will in turn ask for a missing direct object for
* the GetOffOf action, since DOWN can mean getting off of a platform or
* other nested room when on such a thing. When there's an obvious
* thing to get down from, the parser will provide the default
* automatically, which will make DOWN into a simple synonym for GET OFF
* OF <whatever>.
*
* Second, games can use this kind of connector for a given direction
* when the direction is ambiguous. For example, you can use this as
* the 'north' connector when there are two doors leading north from the
* location. When the player types NORTH, the parser will ask which
* door the player wants to go through.
*/
class AskConnector: TravelConnector, ResolveAsker
/*
* The specific travel action to attempt. This must be a TAction -
* an action that takes a direct object (and only a direct object).
* The default is TravelVia, but this should usually be customized
* in each instance to the type of travel appropriate for the
* possible connectors.
*/
travelAction = TravelViaAction
/*
* The list of possible direct objects for the travel action. If
* this is nil, we'll simply treat the direct object of the
* travelAction as completely missing, forcing the parser to either
* find a default or ask the player for the missing object. If the
* travel is limited to a specific set of objects (for example, if
* there are two doors leading north, and we want to ask which one
* to use), this should be set to the list of possible objects; the
* parser will then use the ambiguous noun phrase rules instead of
* the missing noun phrase rules to ask the player for more
* information.
*/
travelObjs = nil
/*
* The phrase to use in the disambiguation question to ask which of
* the travelObjs entries is to be used. The language-specific
* module provides a suitable default, but this should usually be
* overridden if travelObjs is overridden.
*/
travelObjsPhrase = nil
/*
* An extra prompt message to show before the normal parser prompt
* for a missing or ambiguous object. We'll show this just before
* the normal parser message, if it's specified.
*
* If you want to customize the messages more completely, you can
* override askDisambig() or askMissingObject(). The parser will
* invoke these to generate the prompt, so you can customize the
* entire messages by overriding these.
*/
promptMessage = nil
/*
* For each of the ResolveAsker methods that might be invoked, add
* the promptMessage text before the normal parser question.
*/
askDisambig(targetActor, promptTxt, curMatchList, fullMatchList,
requiredNum, askingAgain, dist)
{
promptMessage;
inherited(targetActor, promptTxt, curMatchList, fullMatchList,
requiredNum, askingAgain, dist);
}
askMissingObject(targetActor, action, which)
{
promptMessage;
inherited(targetActor, action, which);
}
/* handle travel via this connector */
dobjFor(TravelVia)
{
/*
* No preconditions or checks are necessary, since we don't
* actually perform any travel on our own; we simply recast the
* command as a new action, hence we want to delegate the
* preconditions and check() handling to the replacement action.
* Note that this means that you can't put a travel barrier
* directly on an AskConnector - you have to put any barriers on
* the underlying real connectors instead.
*/
preCond = []
check() { }
/*
* Recast the travel into our specified action, asking for the
* direct object we need for that action.
*/
action()
{
/*
* if we have a set of possible direct objects, retry this
* with the ambiguous object set; otherwise, retry with a
* completely missing direct object
*/
if (travelObjs != nil)
travelAction.retryWithAmbiguousDobj(
gAction, travelObjs, self, travelObjsPhrase);
else
travelAction.retryWithMissingDobj(gAction, self);
}
}
/*
* Get a connector leading to the given destination. We'll scan our
* travel objects; for each one that's a TravelConnector, we'll ask
* it to find the connector, and return the result if we get one.
*/
connectorGetConnectorTo(origin, traveler, dest)
{
/* if we have no travel objects, there's nothing to check */
if (travelObjs == nil)
return nil;
/* scan our secondary connectors */
foreach (local cur in travelObjs)
{
/* if this is a travel connector, ask it what it thinks */
if (cur.ofKind(TravelConnector))
{
local conn;
/*
* if this secondary connector can give us a connector to
* the destination, use that connector
*/
conn = cur.connectorGetConnectorTo(origin, traveler, dest);
if (conn != nil)
return conn;
}
}
/* didn't find a match */
return nil;
}
;
/*
* A "default ask connector" is an AskConnector that we use for certain
* directions (down, in, out) as the library default connector for the
* directions.
*/
class DefaultAskConnector: AskConnector
/*
* since this is a default connector for all locations, indicate that
* no travel is apparently possible in this direction
*/
isConnectorApparent(origin, actor) { return nil; }
/* this is not a passable connector */
isConnectorPassable(origin, traveler) { return nil; }
;
/*
* A default travel connector for going in. When travel in the relative
* direction "in" isn't allowed, we'll try recasting the command as an
* "enter" command with an unknown direct object.
*/
noTravelIn: DefaultAskConnector
/* when we go 'in', we'll try to ENTER something */
travelAction = EnterAction
;
/*
* A default travel connector for going out. When travel in the
* relative direction "out" isn't allowed, we'll try recasting the
* command as an "get out of" command with an unknown direct object.
*/
noTravelOut: DefaultAskConnector
/* when we go 'out', we'll try to GET OUT OF something */
travelAction = GetOutOfAction
;
/*
* A default travel connector for going out from a nested room. This
* works the same way as noTravelOut, except that we'll show OUT as a
* listed exit.
*/
nestedRoomOut: noTravelOut
isConnectorApparent(origin, actor) { return true; }
;
/*
* A special travel connector for 'down' that recasts the command as a
* "get off of" command. This can be used for platforms and the like,
* where a 'down' command should usually be taken to mean "get off
* platform" rather than "down from enclosing room".
*/
noTravelDown: DefaultAskConnector
/* when we go 'down', we'll try to GET OFF OF something */
travelAction = GetOffOfAction
;
/*
* A travel connector for going in that explicitly redirects the command
* to "enter" and asks for the missing direct object. This behaves the
* same way as noTravelIn, but explicitly makes the inward travel
* apparent; this can be used to override the noTravelIn default for
* locations where travel in is explicitly allowed.
*/
askTravelIn: AskConnector
travelAction = EnterAction
;
/* explicitly redirect travel out to "get out of" */
askTravelOut: AskConnector
travelAction = GetOutOfAction
;
/* explicitly redirect travel down to "get off of" */
askTravelDown: AskConnector
travelAction = GetOffOfAction
;
/*
* "No Shipboard Directions" travel connector. This is used as the
* default connector for the shipboard directions for the base Room
* class. This connector displays a special message indicating that the
* room is not a boat hence the shipboard directions don't work here.
*/
noShipTravel: noTravel
dobjFor(TravelVia)
{
action()
{
/* simply indicate that this direction isn't applicable here */
gLibMessages.notOnboardShip();
}
}
;
/* ------------------------------------------------------------------------ */
/*
* A mix-in class that can be added to objects that also inherit from
* TravelConnector to add a message as the connector is traversed.
*
* Note that this isn't itself a travel connector; it's just a class
* that should be combined with TravelConnector or one of its
* subclasses. This class should be in the superclass list before the
* TravelConnector-derived superclass.
*/
class TravelWithMessage: object
/*
* My message to display when the player character traverses the
* connector. This should be overridden with the custom message for
* the connector. By default, if we're a Script, we'll invoke the
* script to show the next message.
*/
travelDesc()
{
if (ofKind(Script))
doScript();
}
/*
* My message to display when any non-player character traverses the
* connector. If this is not overridden, no message will be
* displayed when an NPC travels through the connector.
*/
npcTravelDesc = ""
/*
* Display my message. By default, we show one message for the
* player character and another message for NPC's.
*/
showTravelDesc()
{
if (gActor.isPlayerChar())
travelDesc;
else
npcTravelDesc;
}
/* on traversing the connector, show our message */
noteTraversal(traveler)
{
/* display my message */
showTravelDesc();
/* inherit any other superclass handling as well */
inherited(traveler);
}
;
/*
* A simple connector that displays a message when the connector is
* traversed.
*/
class TravelMessage: TravelWithMessage, TravelConnector
/* my destination location */
destination = nil
/* get my destination */
getDestination(origin, traveler) { return destination; }
/* our source is the same as our destination */
fixedSource(dest, traveler) { return destination; }
;
/*
* A travel connector that can't be traversed, and which shows a custom
* failure message when traversal is attempted. Instances should define
* travelDesc to the message to display when travel is attempted.
*
* Travel is not apparently possible in this direction, so this type of
* connector will not show up in automatic exit lists or maps. This
* class is designed for connections that are essentially the same as no
* connection at all, but where it's desirable to use a special message
* to describe why travel can't be accomplished.
*/
class NoTravelMessage: TravelMessage
dobjFor(TravelVia)
{
/* as in noTravel, we need no preconditions or checks */
preCond = []
action()
{
/* simply show my message - that's all we do */
showTravelDesc();
}
}
/*
* Because no travel is possible, we want a non-empty message for
* NPC's as well as for the PC; by default, use the same message for
* all actors by using travelDesc for NPC's.
*/
npcTravelDesc = (travelDesc)
/* travel is not apparently possible in this direction */
isConnectorApparent(origin, actor) { return nil; }
isConnectorPassable(origin, traveler) { return nil; }
;
/*
* A "fake" connector. This is a connector that doesn't actually allow
* travel, but acts like it *could*. We simply show a special message
* when travel is attempted, but we don't move the actor.
*
* This is a subclass of NoTravelMessage, so instances should customize
* travelDes with the special failure message when travel is attempted.
*
* Note that this type of connector is by default *apparently* an exit,
* even though it doesn't actually go anywhere. This is useful for "soft
* boundaries," where the game's map is meant to appear to the player to
* continue but beyond which no more locations actually exist in the map.
* This is an oft-used device meant to create an illusion that the game's
* map exists in a larger world even though the larger world is not
* modeled. The message we display should in such cases attribute the
* actor's inability to traverse the connector to a suitable constraint
* within the context of the game world; for example, the actor could be
* said to be unwilling to go beyond this point because the actor knows
* or suspects there's nothing for a long way in this direction, or
* because the actor's goals require staying within the modeled map, or
* because the actor is afraid of what lies beyond.
*
* Note that FakeConnector should only be used when the reason for
* blocking the travel is apparent before we even try travel. In
* particular, FakeConnector is good for motivational reasons not to
* travel, where the actor decides for reasons of its own not to even
* attempt the travel. It's especially important not to use
* FakeConnector in cases where the travel is described as attempted but
* aborted halfway in - things like encountering a blocked tunnel. This
* is important because FakeConnector aborts the travel immediately,
* before sending out any of the notifications that would accompany
* ordinary travel, which means that physical barriers (trolls blocking
* the way, being tied to a chair) that would otherwise block ordinary
* travel will be bypassed. For cases where travel is attempted, but
* something turns you back halfway in, use DeadEndConnector.
*/
class FakeConnector: NoTravelMessage
/*
* travel is *apparently* possible in this direction (even though
* it's not *actually* possible)
*/
isConnectorApparent(origin, actor) { return true; }
isConnectorPassable(origin, traveler) { return nil; }
;
/* ------------------------------------------------------------------------ */
/*
* A Dead End Connector is a connector that appears to lead somewhere,
* but which turns out to be impassable for reasons that aren't apparent
* until we get some distance into the passage.
*
* The Dead End Connector might look a lot like the Fake Connector, but
* there's an important difference. A Fake Connector is a connector that
* can't even be physically attempted: the reason not to take a fake
* connector is something that shows up before we even start moving,
* usually a motivational reason ("You really can't leave town until you
* find your missing brother"). A Dead End Connector, on the other hand,
* is meant to model a physical attempt to travel that's blocked by some
* problem halfway along, such as travel down a tunnel that turns out to
* have caved in.
*/
class DeadEndConnector: TravelMessage
/*
* The apparent destination name. If the actor is meant to know the
* apparent destination from the outset, or if traversing the
* connector gives the actor an idea of where the connector
* supposedly goes, this can be used to give the name of that
* destination. This name will show up in exit listings, for
* example, once the PC knows where the connector supposedly goes.
*
* Note that this isn't the actual destination of the connector,
* since the actual destination is simply back to the origin (that's
* the whole point of the dead end, after all). This is simply where
* the connector *appears* to go. If an attempted traversal doesn't
* even reveal that much, then you should just leave this nil, since
* the destination will never become apparent to the PC.
*/
apparentDestName = nil
/*
* Our apparent destination. By default, we create a FakeDestination
* object to represent our apparent destination if we have a non-nil
* name for the apparent destination.
*
* If the supposed-but-unreachable destination of the connector is in
* fact a real location in the game, you can override this to point
* directly to that actual location. This default is for the typical
* case where the supposed destination doesn't actually exist on the
* game map as a real room.
*/
apparentDest()
{
/*
* if we have an apparent destination name, create a
* FakeDestination to represent the apparent destination, and
* plug that in as our apparent destination for future reference
*/
if (apparentDestName != nil)
{
local fake;
/* create a fake destination */
fake = new FakeDestination(self);
/* plug it in as our new apparentDest for future calls */
apparentDest = fake;
/* return the new object as the result */
return fake;
}
/* our apparent destination is unknown */
return nil;
}
/* get our apparent destination */
getApparentDestination(origin, actor)
{
/*
* If the actor knows the destination for the given connector (as
* determined by the origin room), or we have a memory of this
* traversal, return our fake destination object to represent the
* destination. We can only return our fake destination if we
* have an explicit apparent destination, since it's only in this
* case that our apparent destination ever actually becomes
* known, even after an attempted traversal.
*
* Our actual destination is always just the origin, but we
* *appear* to have some other destination. Even though we can
* never actually reach that other, apparent destination, we at
* least want to give the appearance of going there, which we
* provide through our fake destination object.
*/
if (apparentDest != nil
&& (origin.actorKnowsDestination(actor, self)
|| travelMemory[[actor, origin, self]] != nil))
return apparentDest;
/* our unreachable destination is not apparent */
return nil;
}
/* there's no corresponding connector back for a dead end */
connectorBack(traveler, dest) { return nil; }
/* our actual destination is always our origin */
getDestination(origin, traveler) { return origin; }
/* do remember circular trips, since that's the only kind we make */
rememberCircularPassage = true
;
/*
* A fake apparent destination, for dead-end connectors. The dead-end
* connector will create an object of this class to represent its
* apparent but actually unreachable destination, if it has an apparent
* destination name.
*/
class FakeDestination: object
/* construct - remember our associated connector */
construct(conn) { connector = conn; }
/* get our destination name - this is the name from our connector */
getDestName(actor, origin) { return connector.apparentDestName; }
/* our underlying connector (usually a DeadEndConnector) */
connector = nil
;
/* ------------------------------------------------------------------------ */
/*
* A direct room-to-room connector. In most cases, it is not necessary
* to create one of these objects, because rooms can serve as their own
* connectors. These objects are needed in certain cases, though, such
* as when a room-to-room connection requires a travel barrier.
*/
class RoomConnector: TravelConnector
/* the two rooms we link */
room1 = nil
room2 = nil
/*
* get the destination, given the origin: this is the one of the two
* rooms we link that we're not in now
*/
getDestination(origin, traveler)
{
if (origin == room1)
return room2;
else if (origin == room2)
return room1;
else
return nil;
}
fixedSource(origin, traveler)
{
/*
* we're a symmetrical two-way connector, so our source from the
* perspective of one room is the same as the destination from
* that same perspective
*/
return getDestination(origin, traveler);
}
/*
* Get the precondition for this connector. The normal
* TravelConnector rule that the traveler must be in the outbound
* connector's location is not meaningful for an abstract room
* connector, because this type of connection itself isn't
* represented as a physical game object with a location; it's just
* an abstract data structure. This means that we must have a
* directional property in the *source* location that points directly
* to the destination (i.e., self).
*
* So, the appropriate starting point for room connectors is the
* object that contains the connection. In other words, we must
* search for the nearest object enclosing the *traveler* that has a
* direction property directly linked to 'self'; that enclosing
* container is the required starting location for the travel.
*/
connectorTravelPreCond()
{
/*
* Scan upwards from the traveler's location, looking for an
* object that has a directional property linked to self. Only
* scan into locations that the actor can see.
*/
for (local loc = gActor.getTraveler(self).location ;
loc != nil && gActor.canSee(loc) ; loc = loc.location)
{
/* look for a directional connector directly from 'loc' to us */
if (loc.localDirectionLinkForConnector(self) != nil)
{
/*
* we're linked from this enclosing location, so this is
* where we have to be before travel
*/
return [new TravelerDirectlyInRoom(gActor, self, loc)];
}
}
/* we couldn't find a link, so apply no condition */
return [];
}
;
/*
* A one-way room connector. This works like an ordinary room
* connector, but connects only in one direction. To use this class,
* simply define the 'destination' property to point to the room we
* connect to.
*/
class OneWayRoomConnector: RoomConnector
/* my destination - instances must define this */
destination = nil
/* we always have a fixed destination */
getDestination(origin, traveler) { return destination; }
;
/*
* A room "auto-connector". This is a special subclass of RoomConnector
* that can be mixed in to any BasicLocation subclass to make the room
* usable as the direct target of a directional property in another room
* - so you could say "east = myRoom", for example, without creating any
* intermediate connector.
*/
class RoomAutoConnector: RoomConnector
/*
* Suppose that roomA.north = roomB. This means that if an actor is
* in roomA, and executes a "north" command, we'll execute a
* TravelVia action on room B, because the "connector" will be
* roomB. In these cases, the destination of the travel and the
* travel connector are one and the same. So, when the connector is
* roomB, the destination of travel is also simply roomB.
*/
getDestination(origin, traveler)
{
/* we are our own destination */
return self;
}
;
/* ------------------------------------------------------------------------ */
/*
* Base class for passages between rooms. This can be used for a
* passage that not only connects the rooms but also exists as an object
* in its own right within the rooms it connects.
*
* In most cases, two passage objects will exist - one on each side of
* the passage, so one object in each room connected. One of the
* objects should be designated as the "master"; the other is the
* "slave." The master object is the one which should implement all
* special behavior involving state changes (such as opening or closing
* a door).
*
* This basic passage is not designed to be opened and closed; use Door
* for a passage that can be opened and closed.
*/
class Passage: Linkable, Fixture, TravelConnector
/*
* Our destination - this is where the actor ends up when traveling
* through the passage (assuming the passage is open). By default,
* we return the "room location" of our other side's container; in
* cases where our other side is not directly in our destination
* room (for example, the other side is part of some larger object
* structure), this property should be overridden to specify the
* actual destination.
*
* If our otherSide is nil, the passage doesn't go anywhere. This
* can be useful to create things that look and act like passages,
* but which don't actually go anywhere - in other words,
* passage-like decorations.
*/
destination = (otherSide != nil ? otherSide.location.roomLocation : nil)
/* get my destination - just return my 'destination' property */
getDestination(origin, traveler) { return destination; }
/* get our open/closed status */
isOpen()
{
/*
* if we have a separate master object, defer to it; otherwise,
* use our own status
*/
return (masterObject == self ? isOpen_ : masterObject.isOpen);
}
/* internal open/closed status - open by default */
isOpen_ = true
/*
* We're not visible in the dark if we're closed. If we're open,
* the normal rules apply.
*
* Normally, a passage is visible in the dark if there's light in
* the adjoining location: our model is that enough light is leaking
* in through the passage to make the passage itself visible, but
* not enough to light anything else in the current room. In the
* case of a closed passage, though, we assume that it completely
* blocks any light from the other room, eliminating any indication
* of a passage.
*
* If you do want an openable passage to be visible in the dark even
* when it's closed, it's probably better to make the passage
* self-illuminating (i.e., with brightness 1), because this will
* put the passage in scope and thus allow it to be manipulated.
*/
isConnectorVisibleInDark(origin, actor)
{ return isOpen() && inherited(origin, actor); }
/* a passage is passable when it's open */
isConnectorPassable(origin, traveler) { return isOpen(); }
/*
* Initialize. If we're a slave, we'll set up the otherSide
* relationship between this passage and our master passage.
*/
initializeThing()
{
/* inherit default handling */
inherited();
/*
* if we have a master side, initialize our relationship with
* the master side
*/
if (masterObject != self)
{
/* set our otherSide to point to the master */
otherSide = masterObject;
/* set the master's otherSide to point to us */
masterObject.initMasterObject(self);
}
}
/*
* Initialize the master object. The other side of a two-sided door
* will call this on the master object to let the master object know
* about the other side. 'other' is the other-side object. By
* default, we'll simply remember the other object in our own
* 'otherSide' property.
*/
initMasterObject(other) { otherSide = other; }
/*
* Our corresponding passage object on the other side of the
* passage. This will be set automatically during initialization
* based on the masterObject property of the slave - it is not
* generally necessary to set this manually.
*/
otherSide = nil
/* our other side is the other facet of the passage */
getFacets() { return otherSide != nil ? [otherSide] : inherited(); }
/*
* our source is always our destination, since we have a one-to-one
* relationship with our comlementary passage in the destination
* location (we point to it, it points to us)
*/
fixedSource(origin, traveler) { return destination; }
/* the connector back is our complementary side, if we have one */
connectorBack(traveler, dest)
{
/* if we have a complementary side, it's the connector back */
if (otherSide != nil)
return otherSide;
/* we don't have a complementary side, so use the default handling */
return inherited(traveler, dest);
}
/* can the given actor travel through this passage? */
canActorTravel(actor)
{
/*
* by default, the actor can travel through the passage if and
* only if the passage is open
*/
return isOpen;
}
/*
* Display our message when we don't allow the actor to travel
* through the passage because the passage is closed. By default,
* we'll simply display the default cannotTravel message for the
* actor's location, but this can be overridden to provide a more
* specific report of the problem.
*/
cannotTravel()
{
/* use the actor's location's cannotTravel handling */
gActor.location.cannotTravel();
}
/* carry out travel via this connector */
dobjFor(TravelVia)
{
/* check travel */
check()
{
/*
* Move the actor only if we're open; if we're not, use the
* standard no-travel handling for the actor's location.
* Note that we don't try to implicitly open the passage,
* because the basic passage is not openable; if we're
* closed, it means we're impassable for some reason that
* presumably cannot be remedied by a simple "open" command.
*/
if (!canActorTravel(gActor))
{
/* we're closed, so use our no-travel handling */
cannotTravel();
exit;
}
else
{
/* inherit the default checks */
inherited();
}
}
}
dobjFor(LookThrough)
{
action()
{
/*
* if we're open, we simply "can't see much from here";
* otherwise, we can't see through it at all
*/
if (isOpen)
mainReport(¬hingThroughPassageMsg);
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(¬hingBeyondDoorMsg);
else
inherited();
}
}
/* looking through a door requires it to be open */
dobjFor(LookThrough)
{
preCond = (nilToList(inherited) + objOpen)
}
;
/* ------------------------------------------------------------------------ */
/*
* Secret Door. This is a special kind of door that gives no hint of
* its being a door when it's closed. This can be used for objects that
* serve as a secret passage but are otherwise normal objects, such as a
* bookcase that conceals a passage.
*/
class SecretDoor: BasicDoor
/* a secret passage usually starts off secret (i.e., closed) */
initiallyOpen = nil
isConnectorApparent(origin, actor)
{
/*
* A secret passage is not apparent as a pasage unless it's
* open.
*/
if (isOpen)
{
/* the passage is open - use the inherited handling */
return inherited(origin, actor);
}
else
{
/*
* the passage is closed, so it's not apparently a passage
* at all
*/
return nil;
}
}
;
/*
* Hidden Door. This is a secret door that is invisible when closed.
* This can be used when the passage isn't even visible when it's
* closed, such as a door that seamlessly melds into the wall.
*/
class HiddenDoor: SecretDoor
/*
* When we're closed, we're completely invisible, so we have no
* sight presence. When we're open, we have our normal visual
* presence.
*/
sightPresence = (isOpen)
;
/* ------------------------------------------------------------------------ */
/*
* Automatically closing door. This type of door closes behind an actor
* as the actor traverses the connector.
*/
class AutoClosingDoor: Door
dobjFor(TravelVia)
{
action()
{
/* inherit the default handling */
inherited();
/*
* Only close the door if the actor performing the travel
* isn't accompanying another actor. If the actor is
* accompanying someone, we essentially want them to hold the
* door for the other actor - at the very least, we don't
* want to slam it in the other actor's face!
*/
if (!gActor.curState.ofKind(AccompanyingInTravelState))
{
/* close the door */
makeOpen(nil);
/* mention that the automatic closing */
reportAutoClose();
}
}
}
/*
* Report the automatic closure. The TravelVia action() calls this
* after closing the door to generate a message mentioning that the
* door was closed. By default, we just show the standard
* doorClosesBehindMsg library message.
*/
reportAutoClose() { mainReport(&doorClosesBehindMsg, self); }
;
/* ------------------------------------------------------------------------ */
/*
* The base class for Enterables and Exitables. These are physical
* objects associated with travel connectors. For example, the object
* representing the exterior of a building in the location containing
* the building could be an Enterable, so that typing ENTER BUILDING
* takes us into the building via the travel connector that leads inside.
*
* Enterables and Exitables are physical covers for travel connectors.
* These objects aren't travel connectors themselves, and they don't
* specify the destination; instead, these just point to travel
* connectors.
*/
class TravelConnectorLink: object
/* the underlying travel connector */
connector = nil
/*
* The internal "TravelVia" action just maps to travel via the
* underlying connector. However, we want to apply our own
* preconditions, so we don't directly remap to the underlying
* connector. Instead, we provide our own full TravelVia
* implementation, and then we perform the travel on the underlying
* connector via a replacement action in our own action() handler.
*/
dobjFor(TravelVia)
{
/* the physical link object has to be touchable */
preCond = [touchObj]
verify() { }
action()
{
/* carry out the action by traveling via our connector */
replaceAction(TravelVia, connector);
}
}
/*
* These objects are generally things like buildings (exterior or
* interior), which tend to be large enough that their details can be
* seen at a distance.
*/
sightSize = large
;
/*
* An Enterable is an object that exists in one location, and which can
* be entered to take an actor to another location. Enterables are used
* for things such as the outsides of buildings, so that the building
* can have a presence on its outside and can be entered via a command
* like "go into building".
*
* An Enterable isn't a connector, but points to a connector. This type
* of object is most useful when there's already a connector that exists
* as a separate object, such as the door to a house: the house object
* can be made an Enterable that simply points to the door object.
*/
class Enterable: TravelConnectorLink, Fixture
/*
* "Enter" action this simply causes the actor to travel via the
* connector.
*/
dobjFor(Enter) remapTo(TravelVia, self)
/* explicitly define the push-travel indirect object mapping */
mapPushTravelIobj(PushTravelEnter, TravelVia)
;
/*
* An Exitable is like an Enterable, except that you exit it rather than
* enter it. This can be used for objects representing the current
* location as an enclosure (a jail cell), or an exit door.
*/
class Exitable: TravelConnectorLink, Fixture
/* Get Out Of/Exit action - this simply maps to travel via the connector */
dobjFor(GetOutOf) remapTo(TravelVia, self)
/* explicitly define the push-travel indirect object mapping */
mapPushTravelIobj(PushTravelGetOutOf, TravelVia)
;
/*
* An EntryPortal is just like an Enterable, except that "go through"
* also works on it. Likewise, an ExitPortal is just like an Exitable
* but accepts "go through" as well.
*/
class EntryPortal: Enterable
dobjFor(GoThrough) remapTo(TravelVia, self)
;
class ExitPortal: Exitable
dobjFor(GoThrough) remapTo(TravelVia, self)
;
/* ------------------------------------------------------------------------ */
/*
* A "TravelPushable" is an object that can't be taken, but can be moved
* from one location to another via commands of the form "push obj dir,"
* "push obj through passage," and the like.
*
* TravelPushables tend to be rather rare, and we expect that instances
* will almost always be special cases that require additional
* specialized code. This is therefore only a general framework for
* pushability.
*/
class TravelPushable: Immovable
cannotTakeMsg = &cannotTakePushableMsg
cannotMoveMsg = &cannotMovePushableMsg
cannotPutMsg = &cannotPutPushableMsg
/* can we be pushed via the given travel connector? */
canPushTravelVia(connector, dest) { return true; }
/* explain why canPushTravelVia said we can't be pushed this way */
explainNoPushTravelVia(connector, dest) { }
/*
* Receive notification that we're about to be pushed somewhere.
* This is called just before the underlying traveler performs the
* actual travel. (By default, we don't even define this method, to
* ensure that if we're combined with another base class that
* overrides the method, the overrider will be called.)
*/
// beforeMovePushable(traveler, connector, dest) { }
/*
* Move the object to a new location as part of a push-travel
* operation. By default, this simply uses moveInto to move the
* object, but subclasses can override this to apply conditions to
* the pushing, put the object somewhere else in some cases, display
* extra messages, or do anything else that's necessary.
*
* Our special PushTraveler calls this routine after the underlying
* real traveler has finished its travel to the new location, so the
* traveler's location will indicate the destination of the travel.
* Note that this routine is never called if the traveler ends up in
* its original location after the travel, so this routine isn't
* called when travel isn't allowed for the underlying traveler.
*/
movePushable(traveler, connector)
{
/* move me to the traveler's new location */
moveIntoForTravel(traveler.location);
/* describe what we're doing */
describeMovePushable(traveler, connector);
}
/*
* Describe the actor pushing the object into the new location.
* This is called from movePushable; we pull this out as a separate
* routine so that the description of the pushing can be overridden
* without having to override all of movePushable.
*/
describeMovePushable(traveler, connector)
{
/*
* If the actor is the player character, mention that we're
* pushing the object with us. For an NPC, show nothing,
* because the normal travel message will mention that the NPC
* is pushing the object as part of the normal travel report
* (thanks to PushTraveler's name override).
*/
if (gActor.isPlayerChar)
mainReport(&okayPushTravelMsg, self);
}
dobjFor(PushTravel)
{
verify() { }
action()
{
local newTrav;
local oldTrav;
local wasExcluded;
/*
* Create a push traveler to coordinate the travel. The
* push traveler performs the normal travel, and also moves
* the object being pushed.
*/
newTrav = pushTravelerClass.createInstance(
self, gActor.getPushTraveler(self));
/* set the actor's special traveler to our push traveler */
oldTrav = gActor.setSpecialTraveler(newTrav);
/*
* Add myself to the actor's look-around exclusion list -
* this ensures that we won't be listed among the objects in
* the new location when we show the description on arrival.
* This is desirable because of how we describe the travel:
* we first want to show the new location's description, then
* describe pushing this object into the new location. But
* we actually have to move the object first, to make sure
* any side effects of its presence in the new location are
* taken into account when we describe the location - because
* by the time we're in the room enough to have a look
* around, the pushable will be in the room with us. Even
* so, we don't actually want to describe the pushable as in
* the room at this stage, because from the player's
* perspective we're really still in the process of pushing
* it; describing it as already settled into the new location
* sounds weird because it makes it seem like the object was
* already in place when we arrived. To avoid this
* weirdness, suppress it from the new location's initial
* description on arrival.
*/
wasExcluded = gActor.excludeFromLookAround(self);
/* make sure we undo our global changes when we're done */
try
{
/*
* Now that we've activated our special push traveler
* for the actor, simply perform the ordinary travel
* command. The travel command we perform depends on
* the kind of push-travel command we attempted, so
* simply let the current action carry out the action.
*/
gAction.performTravel();
}
finally
{
/* restore the actor's old special traveler on the way out */
gActor.setSpecialTraveler(oldTrav);
/*
* if we weren't in the actor's 'look around' exclusion
* list before, remove us from the list
*/
if (!wasExcluded)
gActor.unexcludeFromLookAround(self);
}
}
}
/*
* The class we create for our special push traveler - by default,
* this is PushTraveler, but we parameterize this via this property
* to allow special PushTraveler subclasses to be created; this
* could be useful, for example, to customize the traveler name
* messages.
*/
pushTravelerClass = PushTraveler
;
/*
* A special Traveler class for travel involving pushing an object from
* one room to another. This class encapsulates the object being pushed
* and the actual Traveler performing the travel.
*
* For the most part, we refer Traveler methods to the underlying
* Traveler. We override a few methods to provide special handling.
*/
class PushTraveler: object
construct(obj, traveler)
{
/* remember the object being pushed and the real traveler */
obj_ = obj;
traveler_ = traveler;
}
/* the object being pushed */
obj_ = nil
/*
* the underlying Traveler - this is the real Traveler that will
* move to a new location
*/
traveler_ = nil
/*
* Travel to a new location. We'll run the normal travel routine
* for the underlying real traveler; then, if we ended up in a new
* location, we'll move the object being pushed to the traveler's
* new location.
*/
travelerTravelTo(dest, connector, backConnector)
{
local oldLoc;
local origin;
/* remember the traveler's origin, so we can tell if we moved */
origin = traveler_.location;
/* let the object being pushed describe the departure */
obj_.beforeMovePushable(traveler_, connector, dest);
/*
* *Tentatively* move the pushable to its new location. Just do
* the basic move-into for this, since it's only tentative - this
* is just so that we get any side effects of its presence in the
* new location for the purposes of describing the new location.
*/
oldLoc = location;
obj_.baseMoveInto(dest);
/*
* Call the traveler's normal travel routine, to perform the
* actual movement of the underlying traveler.
*/
traveler_.travelerTravelTo(dest, connector, backConnector);
/* undo the tentative move of the pushable */
obj_.baseMoveInto(oldLoc);
/*
* If we moved to a new location, we can now actually move the
* pushable object to the new location.
*/
if (traveler_.location != origin)
obj_.movePushable(traveler_, connector);
}
/*
* Perform local travel, between nested rooms within a top-level
* location. By default, we simply don't allow pushing objects
* between nested rooms.
*
* To allow pushing an object between nested rooms, override this in
* parallel with travelerTravelTo(). Note that you'll have to call
* travelerTravelWithin() on the underlying traveler (which will
* generally be the actor), and you'll probably want to set up a new
* set of notifiers parallel to beforeMovePushable() and
* movePushable(). You'll probably particularly need to customize
* the report in your parallel for movePushable() - the default ("you
* push x into the area") isn't very good when nested rooms are
* involved, and you'll probably want something more specific.
*/
travelerTravelWithin(actor, dest)
{
reportFailure(&cannotPushObjectNestedMsg, obj_);
exit;
}
/*
* Can we travel via the given connector? We'll ask our underlying
* traveler first, and if that succeeds, we'll ask the object we're
* pushing.
*/
canTravelVia(connector, dest)
{
/* ask the underlying traveler first, then our pushed object */
return (traveler_.canTravelVia(connector, dest)
&& obj_.canPushTravelVia(connector, dest));
}
/*
* Explain why the given travel is not possible. If our underlying
* traveler raised the objection, let it explain; otherwise, let our
* pushed object explain.
*/
explainNoTravelVia(connector, dest)
{
if (!traveler_.canTravelVia(connector, dest))
traveler_.explainNoTravelVia(connector, dest);
else
obj_.explainNoPushTravelVia(connector, dest);
}
/* by default, send everything to the underlying Traveler */
propNotDefined(prop, [args]) { return traveler_.(prop)(args...); }
;
/*
* A PushTravelBarrier is a TravelConnector that allows regular travel,
* but not travel that involves pushing something. By default, we block
* all push travel, but subclasses can customize this so that we block
* only specific objects.
*/
class PushTravelBarrier: TravelBarrier
/*
* Determine if the given pushed object is allowed to pass. Returns
* true if so, nil if not. By default, we'll return nil for every
* object; subclasses can override this to allow some objects to be
* pushed through the barrier but not others.
*/
canPushedObjectPass(obj) { return nil; }
/* explain why an object can't pass */
explainTravelBarrier(traveler)
{
reportFailure(&cannotPushObjectThatWayMsg, traveler.obj_);
}
/*
* Determine if the given traveler can pass through this connector.
* If the traveler isn't a push traveler, we'll allow the travel;
* otherwise, we'll block the travel if our canPushedObjectPass
* routine says the object being pushed can pass.
*/
canTravelerPass(traveler)
{
/* if it's not a push traveler, it can pass */
if (!traveler.ofKind(PushTraveler))
return true;
/* it can pass if we can pass the object being pushed */
return canPushedObjectPass(traveler.obj_);
}
;
/* ------------------------------------------------------------------------ */
/*
* A basic location - this is the base class for locations that can
* contain actors.
*/
class BasicLocation: Thing
/*
* Get the nested room list grouper for an actor in the given
* posture directly in this room. This is used when we're listing
* the actors within the nested room as
*
* By default, we maintain a lookup table, and store one nested
* actor grouper object for each posture. This makes it so that we
* show one group per posture in this room; for example, if we
* contain two sitting actors and three standing actors, we'll say
* something like "bill and bob are sitting on the stage, and jill,
* jane, and jack are standing on the stage." This can be
* overridden if a different arrangement of groups is desired; for
* example, an override could simply return a single grouper to list
* everyone in the room together, regardless of posture.
*/
listWithActorIn(posture)
{
/* if we don't have a lookup table for this yet, create one */
if (listWithActorInTable == nil)
listWithActorInTable = new LookupTable(5, 5);
/* if this posture isn't in the table yet, create a grouper for it */
if (listWithActorInTable[posture] == nil)
listWithActorInTable[posture] =
new RoomActorGrouper(self, posture);
/* return the grouper for this posture */
return listWithActorInTable[posture];
}
/*
* our listWithActorIn table - this gets initialized to a
* LookupTable as soon as we need one (in listWithActorIn)
*/
listWithActorInTable = nil
/*
* Check the ambient illumination level in the room for the given
* actor's senses to determine if the actor would be able to see if
* the actor were in this room without any additional light sources.
* Returns true if the room is lit for the purposes of the actor's
* visual senses, nil if not.
*
* Note that if the actor is already in this location, there's no
* need to run this test, since we could get the information from
* the actor directly. The point of this test is to determine the
* light level in this location without the actor having to be
* present.
*/
wouldBeLitFor(actor)
{
/*
* Check for a simple, common case before running the more
* expensive full what-if test. Most rooms provide their own
* illumination in the actor's 'sight' sense; if this room does
* provide its own interior 'brightness' of at least 2, and the
* actor has 'sight' among its visual senses, then the room is
* lit. Note that we must use transSensingOut() to determine
* how we transmit our own brightness to our interior, since
* that gives the transparency for looking from within this room
* at objects outside the room; our own intrinsic brightness is
* defined as the brightness of our exterior surface.
*/
if (actor.sightlikeSenses.indexOf(sight) != nil
&& brightness > 1
&& transSensingOut(sight) == transparent)
return true;
/*
* We can't determine for sure that the location is lit with a
* simple test, so run a full what-if test, checking what
* ambient light level our "probe" object would see if it were
* moved to this location. Return true if the ambient light
* level is higher than the "self-lit" value of 1, nil if not.
*/
return (lightProbe.whatIf(
{: senseAmbientMax(actor.sightlikeSenses) },
&moveInto, self) > 1);
}
/*
* Show our room description: this is the interior description of
* the room, for use when the room is viewed by an actor within the
* room. By default, we show our ordinary 'desc'.
*/
roomDesc { desc; }
/* as part of a room description, mention an actor in this room */
roomActorHereDesc(actor) { gLibMessages.actorInRoom(actor, self); }
/*
* Provide a default description for an actor in this location, as
* seen from a remote location (i.e., from a separate top-level room
* that's linked to our top-level room by a sense connector of some
* kind). By default, we'll describe the actor as being in this
* nested room.
*/
roomActorThereDesc(actor)
{
local pov = getPOV();
local outer;
/* get the outermost visible enclosing room */
outer = getOutermostVisibleRoom(pov);
/*
* If we found a room, and it's not us (i.e., we found something
* outside this room that can be seen from the current point of
* view), use the three-part description: actor in nested room
* (self) in outer room. If we didn't find a room, or it's the
* same as us, all we can say is that the actor is in the nested
* room.
*/
if (outer not in (nil, self))
{
/* use the three-part description: actor in self in outer */
gLibMessages.actorInRemoteNestedRoom(actor, self, outer, pov);
}
else
{
/* we're as far out as we can see: just say actor is in self */
gLibMessages.actorInRemoteRoom(actor, self, pov);
}
}
/* show the status addendum for an actor in this location */
roomActorStatus(actor) { gLibMessages.actorInRoomStatus(actor, self); }
/* describe the actor's posture while in this location */
roomActorPostureDesc(actor)
{ gLibMessages.actorInRoomPosture(actor, self); }
/* acknowledge a posture change while in this location */
roomOkayPostureChange(actor)
{ defaultReport(&roomOkayPostureChangeMsg, actor.posture, self); }
/*
* describe the actor's posture as part of the EXAMINE description of
* the nested room
*/
roomListActorPosture(actor)
{ gLibMessages.actorInRoom(actor, self); }
/*
* Prefix and suffix messages for listing a group of actors
* nominally in this location. 'posture' is the posture of the
* actors. 'remote' is the outermost visible room containing the
* actors, but only if that room is remote from the point-of-view
* actor; if everything is local, this will be nil. 'lst' is the
* list of actors being listed. By default, we'll just show the
* standard library messages.
*/
actorInGroupPrefix(pov, posture, remote, lst)
{
if (remote == nil)
gLibMessages.actorInGroupPrefix(posture, self, lst);
else
gLibMessages.actorInRemoteGroupPrefix(
pov, posture, self, remote, lst);
}
actorInGroupSuffix(pov, posture, remote, lst)
{
if (remote == nil)
gLibMessages.actorInGroupSuffix(posture, self, lst);
else
gLibMessages.actorInRemoteGroupSuffix(
pov, posture, self, remote, lst);
}
/*
* Show a list of exits from this room as part of failed travel
* ("you can't go that way").
*/
cannotGoShowExits(actor)
{
/* if we have an exit lister, ask it to show exits */
if (gExitLister != nil)
gExitLister.cannotGoShowExits(actor, self);
}
/* show the exit list in the status line */
showStatuslineExits()
{
/* if we have a global exit lister, ask it to show the exits */
if (gExitLister != nil)
gExitLister.showStatuslineExits();
}
/*
* Get the estimated height, in lines of text, of the exits display's
* contribution to the status line. This is used to calculate the
* extra height we need in the status line, if any, to display the
* exit list. If we're not configured to display exits in the status
* line, this should return zero.
*/
getStatuslineExitsHeight()
{
if (gExitLister != nil)
return gExitLister.getStatuslineExitsHeight();
else
return 0;
}
/*
* Make the actor stand up from this location. By default, we'll
* simply change the actor's posture to "standing," and show a
* default success report.
*
* Subclasses might need to override this. For example, a chair
* will set the actor's location to the room containing the chair
* when the actor stands up from the chair.
*/
makeStandingUp()
{
/* simply set the actor's new posture */
gActor.makePosture(standing);
/* issue a default report of the change */
defaultReport(&okayPostureChangeMsg, standing);
}
/*
* Default posture for an actor in the location. This is the
* posture assumed by an actor when moving out of a nested room
* within this location.
*/
defaultPosture = standing
/* failure report we issue when we can't return to default posture */
mustDefaultPostureProp = &mustBeStandingMsg
/* run the appropriate implied command to achieve our default posture */
tryMakingDefaultPosture()
{
return defaultPosture.tryMakingPosture(self);
}
/*
* Check this object as a staging location. We're a valid location,
* so we allow this.
*/
checkStagingLocation(dest)
{
/* we've valid, so we don't need to do anything */
}
/*
* Try moving the actor into this location.
*/
checkMovingActorInto(allowImplicit)
{
/*
* If the actor isn't somewhere within us, we can't move the
* actor here implicitly - we have no generic way of causing
* implicit travel between top-level locations. Note that some
* rooms might want to override this when travel between
* adjacent locations makes sense as an implicit action; we
* expect that such cases will be rare, so we don't attempt to
* generalize this possibility.
*/
if (!gActor.isIn(self))
{
reportFailure(&cannotDoFromHereMsg);
exit;
}
/*
* if the actor is already directly in me, simply check to make
* sure the actor is in the default posture for the room - if
* not, try running an appropriate implied command to change the
* posture
*/
if (gActor.isDirectlyIn(self))
{
/* if the actor's already in the default posture, we're okay */
if (gActor.posture == defaultPosture)
return nil;
/* run the implied command to stand up (or whatever) */
if (allowImplicit && tryMakingDefaultPosture())
{
/* make sure we're in the proper posture now */
if (gActor.posture != defaultPosture)
exit;
/* note that we ran an implied command */
return true;
}
/* we couldn't get into the default posture - give up */
reportFailure(mustDefaultPostureProp);
exit;
}
/*
* The actor is within a nested room within me. Find our
* immediate child containing the actor, and remove the actor
* from the child.
*/
foreach (local cur in contents)
{
/* if this is the one containing the actor, remove the actor */
if (gActor.isIn(cur))
return cur.checkActorOutOfNested(allowImplicit);
}
/* we didn't find the nested room with the actor, so give up */
reportFailure(&cannotDoFromHereMsg);
exit;
}
/*
* Check, using pre-condition rules, that the actor is ready to
* enter this room as a nested location. By default, we do nothing,
* since we're not designed as a nested location.
*/
checkActorReadyToEnterNestedRoom(allowImplicit)
{
return nil;
}
/*
* Check that the traveler is directly in the given room, using
* pre-condition rules. 'nested' is the nested location immediately
* within this room that contains the actor (directly or
* indirectly).
*/
checkTravelerDirectlyInRoom(traveler, allowImplicit)
{
/* if the actor is already directly in this room, we're done */
if (traveler.isDirectlyIn(self))
return nil;
/* try moving the actor here */
return traveler.checkMovingTravelerInto(self, allowImplicit);
}
/*
* Check, using pre-condition rules, that the actor is removed from
* this nested location and moved to its exit destination. By
* default, we're not a nested location, so there's nothing for us
* to do.
*/
checkActorOutOfNested(allowImplicit)
{
/* we're not a nested location, so there's nothing for us to do */
return nil;
}
/*
* Determine if the current gActor, who is directly in this location,
* is "travel ready." This means that the actor is ready, as far as
* this location is concerned, to traverse the given connector. By
* default, we consider an actor to be travel-ready if the actor is
* standing; this takes care of most nested room situations, such as
* chairs and beds, automatically.
*/
isActorTravelReady(conn) { return gActor.posture == standing; }
/*
* Run an implicit action, if possible, to make the current actor
* "travel ready." This will be called if the actor is directly in
* this location and isActorTravelReady() returns nil. By default,
* we try to make the actor stand up. This should always be paired
* with isActorTravelReady - the condition that routine tests should
* be the condition this routine tries to bring into effect. If no
* implicit action is possible, simply return nil.
*/
tryMakingTravelReady(conn) { return tryImplicitAction(Stand); }
/* the message explaining what we must do to be travel-ready */
notTravelReadyMsg = &mustBeStandingMsg
/*
* An actor is attempting to disembark this location. By default,
* we'll simply turn this into an "exit" command.
*/
disembarkRoom()
{
/* treat this as an 'exit' command */
replaceAction(Out);
}
/*
* The destination for objects explicitly dropped by an actor within
* this room. By default, we'll return self, because items dropped
* should simply go in the room itself. Some types of rooms will
* want to override this; for example, a room that represents the
* middle of a tightrope would probably want to set the drop
* destination to the location below the tightrope. Likewise,
* objects like chairs will usually prefer to have dropped items go
* into the enclosing room.
*/
getDropDestination(objToDrop, path)
{
/* by default, objects dropped in this room end up in this room */
return self;
}
/*
* The nominal drop destination - this is the location where objects
* are *reported* to go when dropped by an actor in this location.
* By default, we simply return 'self'.
*
* The difference between the actual drop location and the nominal
* drop location is that the nominal drop location is used only for
* reporting messages, while the actual drop location is the
* location where objects are moved on 'drop' or equivalent actions.
* Rooms, for example, want to report that a dropped object lands on
* the floor (or the ground, or whatever), even though the room
* itself is the location where the object actually ends up. We
* distinguish between the nominal and actual drop location to allow
* these distinctions in reported messages.
*/
getNominalDropDestination() { return self; }
/*
* The "nominal actor container" - this is the container which we'll
* say actors are in when we describe actors who are actually in
* this location. By default, this simply returns self, but it's
* sometimes useful to describe actors as being in some object other
* than self. The most common case is that normal top-level rooms
* usually want to describe actors as being "on the floor" or
* similar.
*/
getNominalActorContainer(posture) { return self; }
/*
* Get any extra items in scope for an actor in this location.
* These are items that are to be in scope even if they're not
* reachable through any of the normal sense paths (so they'll be in
* scope even in the dark, for example).
*
* By default, this returns nothing. Subclasses can override as
* necessary to include additional items in scope. For example, a
* chair would probably want to include itself in scope, since the
* actor presumably knows he or she is sitting in a chair even if
* it's too dark to see the chair.
*/
getExtraScopeItems(actor) { return []; }
/*
* Receive notification that we're about to perform a command within
* this location. This is called on the outermost room first, then
* on the nested rooms, from the outside in, until reaching the room
* directly containing the actor performing the command.
*/
roomBeforeAction()
{
}
/*
* Receive notification that we've just finished a command within
* this location. This is called on the room immediately containing
* the actor performing the command, then on the room containing
* that room, and so on to the outermost room.
*/
roomAfterAction()
{
}
/*
* Get my notification list - this is a list of objects on which we
* must call beforeAction and afterAction when an action is
* performed within this room.
*
* We'll also include any registered notification items for all of
* our containing rooms up to the outermost container.
*
* The general notification mechanism always includes in the
* notification list all of the objects connected by containment to
* the actor, so objects that are in this room need not register for
* explicit notification.
*/
getRoomNotifyList()
{
local lst;
/* start with our explicitly registered list */
lst = roomNotifyList;
/* add notification items for our immediate locations */
forEachContainer(
{cont: lst = lst.appendUnique(cont.getRoomNotifyList())});
/* return the result */
return lst;
}
/*
* Add an item to our registered notification list for actions in
* the room.
*
* Items can be added here if they must be notified of actions
* performed by within the room even when the items aren't in the
* room at the time of the action. All items connected by
* containment with the actor performing an action are automatically
* notified of the action; only items that must receive notification
* even when not connected by containment need to be registered
* here.
*/
addRoomNotifyItem(obj)
{
roomNotifyList += obj;
}
/* remove an item from the registered notification list */
removeRoomNotifyItem(obj)
{
roomNotifyList -= obj;
}
/* our list of registered notification items */
roomNotifyList = []
/*
* Get the room location. Since we're capable of holding actors, we
* are our own room location.
*/
roomLocation = (self)
/*
* Receive notification that a traveler is arriving. This is a
* convenience method that rooms can override to carry out side
* effects of arrival. This is called just before the room's
* arrival message (usually the location description) is displayed,
* so the method can make any adjustments to the room's status or
* contents needed for the arrival. By default, we do nothing.
*/
enteringRoom(traveler) { }
/*
* Receive notification that a traveler is leaving. This is a
* convenience method that rooms can override to carry out side
* effects of departure. This is called just after any departure
* message is displayed. By default, we do nothing.
*/
leavingRoom(traveler) { }
/*
* Receive notification that a traveler is about to leave the room.
* 'traveler' is the object actually traveling. In most cases this
* is simply the actor; but when the actor is in a vehicle, this is
* the vehicle instead.
*
* By default, we describe the traveler's departure if the traveler's
* destination is different from its present location.
*/
travelerLeaving(traveler, dest, connector)
{
/* describe the departure */
if (dest != traveler.location)
traveler.describeDeparture(dest, connector);
/* run the departure notification */
leavingRoom(traveler);
}
/*
* Receive notification that a traveler is arriving in the room.
* 'traveler' is the object actually traveling. In most cases this
* is simply the actor; but when the actor is in a vehicle, this is
* the vehicle instead.
*
* By default, we set each of the "motive" actors to its default
* posture, then describe the arrival.
*/
travelerArriving(traveler, origin, connector, backConnector)
{
/*
* Set the self-motive actors into the proper default posture
* for the location. We only do this for actors moving under
* their own power, since actors in vehicles will presumably
* just stay in the posture that's appropriate for the vehicle.
*/
foreach (local actor in traveler.getTravelerMotiveActors)
{
/*
* If the actor isn't in this posture already, set the actor
* to the new posture. Note that we set the new posture
* directly, rather than via a nested command; we don't want
* the travel to consist of a NORTH plus a SIT, but simply of
* a NORTH. Note that this could bypass side effects
* normally associated with the SIT (or whatever), but we
* assume that when a room with a specific posture is linked
* directly from a separate location, the travel connector
* linking up to the new room will take care of the necessary
* side effects.
*/
if (actor.posture != defaultPosture)
actor.makePosture(defaultPosture);
}
/* run the arrival notification */
enteringRoom(traveler);
/* describe the arrival */
traveler.describeArrival(origin, backConnector);
}
/*
* Receive notification of travel among nested rooms. When an actor
* moves between two locations related directly by containing (such
* as from a chair to the room containing the chair, or vice versa),
* we first call this routine on the origin of the travel, then we
* move the actor, then we call this same routine on the destination
* of the travel.
*
* This routine is used any time an actor is moved with
* travelWithin(). This is not used when an actor travels between
* locations related by a TravelConnector object rather than by
* direct containment.
*
* We do nothing by default. Locations can override this if they
* wish to perform any special handling during this type of travel.
*/
actorTravelingWithin(origin, dest)
{
}
/*
* Determine if the given actor has "intrinsic" knowledge of the
* destination of the given travel connector leading away from this
* location. This knowledge is independent of any memory the actor
* has of actual travel through the connector in the course of the
* game, which we track separately via the TravelConnector's travel
* memory mechanism.
*
* There are two main reasons an actor would have intrinsic
* knowledge of a connector's destination:
*
* 1. The actor is supposed to be familiar with the location and its
* surroundings, within the context of the game. For example, if
* part of the game is the player character's own house, the PC
* would probably know where all of the connections among rooms go.
*
* 2. The destination location is plainly visible from this location
* or is clearly marked (such as with a sign). For example, if the
* current location is an open field, a nearby hilltop to the east
* might be visible from here, so we could see from here where we'll
* end up by going east. Alternatively, if we're in a lobby, and
* the passage to the west is marked with a sign reading "electrical
* room," an actor would have good reason to think an electrical
* room lies to the west.
*
* We handle case (1) automatically through our actorIsFamiliar()
* method: if the actor is familiar with the location, we assume by
* default that the actor knows where all of the connectors from
* here go. We don't have any default handling for case (2), so
* individual rooms (or subclasses) must override this method if
* they want to specify intrinsic knowledge for any of their
* outgoing connectors.
*/
actorKnowsDestination(actor, conn)
{
/*
* if the actor is familiar with this location, then the actor
* by default knows where all of the outgoing connections go
*/
if (actorIsFamiliar(actor))
return true;
/* there's no other way the actor would know the destination */
return nil;
}
/*
* Is the actor familiar with this location? In other words, is the
* actor supposed to know the location well at the start of the game?
*
* This should return true if the actor is familiar with this
* location, nil if not. By default, we return nil, since actors
* are not by default familiar with any locations.
*
* The purpose of this routine is to determine if the actor is meant
* to know the location well, within the context of the game, even
* before the game starts. For example, if an area in the game is
* an actor's own house, the actor would naturally be familiar,
* within the context of the game, with the locations making up the
* house.
*
* Note that this routine doesn't need to "learn" based on the
* events of the game. The familiarity here is meant only to model
* the actor's knowledge as of the start of the game.
*/
actorIsFamiliar(actor) { return nil; }
/*
* The default "you can't go that way" message for travel within this
* location in directions that don't allow travel. This is shown
* whenever an actor tries to travel in one of the directions we have
* set to point to noTravel. A room can override this to produce a
* different, customized message for unset travel directions - this
* is an easy way to change the cannot-travel message for several
* directions at once.
*
* The handling depends on whether or not it's dark. If it's dark,
* we don't want to reveal whether or not it's actually possible to
* perform the travel, since there's no light to see where the exits
* are.
*/
cannotTravel()
{
/* check for darkness */
if (!gActor.isLocationLit())
{
/* the actor is in the dark - use our dark travel message */
cannotGoThatWayInDark();
}
else
{
/* use the standard "can't go that way" routine */
cannotGoThatWay();
}
}
/*
* Receive notification of travel from one dark location to another.
* This is called before the actor is moved from the source
* location, and can cancel the travel if desired by using 'exit' to
* terminate the command.
*
* By default, we'll simply display the same handler we do when the
* player attempts travel in a direction with no travel possible in
* the dark (cannotGoThatWayInDark), and then use 'exit' to cancel
* the command. This default behavior provides the player with no
* mapping information in the dark, since the same message is
* generated whether or not travel would be possible in a given
* direction were light present.
*/
roomDarkTravel(actor)
{
/*
* show the same message we would show if we attempted travel in
* the dark in a direction with no exit
*/
cannotGoThatWayInDark();
/* terminate the command */
exit;
}
/*
* Show the default "you can't go that way" message for this
* location. By default, we show a generic message, but individual
* rooms might want to override this to provide a more specific
* description of why travel isn't allowed.
*/
cannotGoThatWay()
{
/* "you can't go that way" */
reportFailure(cannotGoThatWayMsg);
/* show a list of exits, if appropriate */
cannotGoShowExits(gActor);
}
/*
* The message to display when it's not possible to travel in a given
* direction from this room; this is either a single-quoted string or
* an actor action messages property (by default, it's the latter,
* giving a default library message).
*/
cannotGoThatWayMsg = &cannotGoThatWayMsg
/*
* Show a version of the "you can't go that way" message for travel
* while in the dark. This is called when the actor is in the dark
* (i.e., there's no ambient light at the actor) and attempts to
* travel in a direction that doesn't allow travel. By default, we
* show a generic "you can't see where you're going in the dark"
* message.
*
* This routine is essentially a replacement for the
* cannotGoThatWay() routine that we use when the actor is in the
* dark.
*/
cannotGoThatWayInDark()
{
/* "it's too dark; you can't see where you're going */
reportFailure(&cannotGoThatWayInDarkMsg);
}
/*
* Get preconditions for travel for an actor in this location. These
* preconditions should be applied by any command that will involve
* travel from this location. By default, we impose no additional
* requirements.
*/
roomTravelPreCond = []
/*
* Get the effective location of an actor directly within me, for
* the purposes of a "follow" command. To follow someone, we must
* have the same effective follow location that the target had when
* we last observed the target leaving.
*
* For most rooms, this is simply the room itself.
*/
effectiveFollowLocation = (self)
/*
* Dispatch the room daemon. This is a daemon routine invoked once
* per turn; we in turn invoke roomDaemon on the current player
* character's current location.
*/
dispatchRoomDaemon()
{
/* call roomDaemon on the player character's location */
if (gPlayerChar.location != nil)
gPlayerChar.location.roomDaemon();
}
;
/* ------------------------------------------------------------------------ */
/*
* Room: the basic class for top-level game locations (that is, game
* locations that aren't inside any other simulation objects, but are at
* the top level of the containment hierarchy). This is the smallest
* unit of movement; we do not distinguish among locations within a
* room, even if a Room represents a physically large location. If it
* is necessary to distinguish among different locations in a large
* physical room, simply divide the physical room into sections and
* represent each section with a separate Room object.
*
* A Room is not necessarily indoors; it is simply a location where an
* actor can be located. This peculiar usage of "room" to denote any
* atomic location, even outdoors, was adopted by the authors of the
* earliest adventure games, and has been the convention ever since.
*
* A room's contents are the objects contained directly within the room.
* These include fixed features of the room as well as loose items in
* the room, which are effectively "on the floor" in the room.
*
* The Room class implements the Travel Connector interface in such a
* way that travel from one room to another can be established simply by
* setting a direction property (north, south, etc) in the origin room
* to point to the destination room. This type of travel link has no
* side effects and is unconditional.
*
* A room is by default an indoor location; this means that it contains
* walls, floor, and ceiling. An outdoor location should be based on
* OutdoorRoom rather than Room.
*/
class Room: Fixture, BasicLocation, RoomAutoConnector
/*
* Initialize
*/
initializeThing()
{
/* inherit default handling */
inherited();
/*
* Add my room parts to my contents list. Only include room
* parts that don't have explicit locations.
*/
contents += roomParts.subset({x: x.location == nil});
}
/*
* we're a "top-level" location: we don't have any other object
* containing us, but we're nonetheless part of the game world, so
* we're at the top level of the containment tree
*/
isTopLevel = true
/*
* we generally do not want rooms to be included when a command
* refers to 'all'
*/
hideFromAll(action) { return true; }
/* don't consider myself a default for STAND ON, SIT ON, or LIE ON */
hideFromDefault(action)
{
/* don't hide from STAND ON, SIT ON, LIE ON */
if (action.ofKind(StandOnAction)
|| action.ofKind(SitOnAction)
|| action.ofKind(LieOnAction))
return nil;
/* don't hide from defaults for other actions, though */
return inherited(action);
}
/*
* Most rooms provide their own implicit lighting. We'll use
* 'medium' lighting (level 3) by default, which provides enough
* light for all activities, but is reduced to dim light (level 2)
* when it goes through obscuring media or over distance.
*/
brightness = 3
/*
* Get my "destination name," as seen by the given actor from the
* given origin location. This gives the name we can use to
* describe this location from the perspective of an actor in an
* adjoining location looking at a travel connector from that
* location to here.
*
* By default, we simply return our destName property. This default
* behavior can be overridden if it's necessary for a location to
* have different destination names in different adjoining
* locations, or when seen by different actors.
*
* If this location's name cannot or should not be described from an
* adjoining location, this should simply return nil.
*/
getDestName(actor, origin) { return destName; }
/*
* Our destination name, if we have one. By default, we make this
* nil, which means the room cannot be described as a destination of
* connectors from adjoining locations.
*/
destName = nil
/*
* My "atmosphere" list. This can be set to an EventList object to
* provide atmosphere messages while the player character is within
* this room. The default roomDaemon will show one message from this
* EventList (by calling the EventList's doScript() method) on each
* turn the player character is in this location.
*/
atmosphereList = nil
/*
* Room daemon - this is invoked on the player character's immediate
* location once per turn in a daemon.
*/
roomDaemon()
{
/*
* if we have an atmosphere message list, display the next
* message
*/
if (atmosphereList != nil)
{
/* show visual separation, then the current atmosphere message */
"<.commandsep>";
atmosphereList.doScript();
}
}
/*
* The nominal drop destination - this is the location where we
* describe objects as being when they're actually directly within
* the room.
*
* By default, we return the object representing the room's floor.
* If there's no floor, we simply return 'self'.
*/
getNominalDropDestination()
{
local floor;
/*
* if there's a floor, it's the nominal drop destination;
* otherwise, just indicate that our contents are in 'self'
*/
return ((floor = roomFloor) != nil ? floor : self);
}
/*
* Since we could be our own nominal drop destination, we need a
* message to describe things being put here.
*/
putDestMessage = &putDestRoom
/*
* The nominal actor container. By default, this is the room's
* nominal drop destination, which is usually the floor or
* equivalent.
*/
getNominalActorContainer(posture) { return getNominalDropDestination(); }
/*
* move something into a room is accomplished by putting the object
* on the floor
*/
tryMovingObjInto(obj)
{
local floor;
/* if we have a floor, put the object there */
if ((floor = roomFloor) != nil)
return tryImplicitAction(PutOn, obj, floor);
else
return nil;
}
/* explain that something must be in the room first */
mustMoveObjInto(obj)
{
local floor;
/* if we have a floor, say that the object has to go there */
if ((floor = roomFloor) != nil)
reportFailure(&mustBeInMsg, obj, floor);
else
inherited(obj);
}
/*
* Get the apparent location of one of our room parts.
*
* In most cases, we use generic objects (defaultFloor,
* defaultNorthWall, etc.) for the room parts. There's only one
* instance of each of these generic objects in the whole game -
* there's only one floor, one north wall, and so on - so these
* instances can't have specific locations the way normal objects do.
* Thus the need for this method: this tells us the *apparent*
* location of one of the room part objects as perceived from this
* room.
*
* If the part isn't in this location, we'll return nil.
*/
getRoomPartLocation(part)
{
local loc;
/*
* if this part is in our part list, then its apparent location
* is 'self'
*/
if (roomParts.indexOf(part) != nil)
return self;
/*
* if the room part has an explicit location itself, and that
* location is either 'self' or is in 'self', return the
* location
*/
if (part != nil
&& (loc = part.location) != nil
&& (loc == self || loc.isIn(self)))
return loc;
/* we don't have the part */
return nil;
}
/*
* Get the list of extra parts for the room. An indoor location has
* walls, floor, and ceiling; these are all generic objects that are
* included for completeness only.
*
* A room with special walls, floor, or ceiling should override this
* to eliminate the default objects from appearing in the room.
* Note that if the room has a floor, it should always be
* represented by an object of class Floor, and should always be
* part of this list.
*/
roomParts = [defaultFloor, defaultCeiling,
defaultNorthWall, defaultSouthWall,
defaultEastWall, defaultWestWall]
/*
* Get the room's floor. This looks for an object of class Floor in
* the roomParts list; if there is no such object, we'll return nil,
* indicating that the room has no floor at all.
*/
roomFloor = (roomParts.valWhich({x: x.ofKind(Floor)}))
/*
* Get any extra items in scope in this location. These are items
* that are to be in scope even if they're not reachable through any
* of the normal sense paths (so they'll be in scope even in the
* dark, for example).
*
* By default, if we have a floor, and the actor is directly in this
* room, we return our floor: this is because an actor is presumed to
* be in physical contact with the floor whenever directly in the
* room, and thus the actor would be aware that there's a floor
* there, even if the actor can't see the floor. In some rooms, it's
* desirable to have certain objects in scope because they're
* essential features of the room; for example, a location that is
* part of a stairway would probably have the stairs in scope by
* virtue of the actor standing on them.
*/
getExtraScopeItems(actor)
{
local floor;
/*
* if we have a floor, and the actor is in this room, explicitly
* make the floor in scope
*/
if ((floor = roomFloor) != nil && actor.isDirectlyIn(self))
return [floor];
else
return [];
}
/*
* When we're in the room, treat EXAMINE <ROOM> the same as LOOK
* AROUND. (This will only work if the room is given vocabulary
* like a normal object.)
*/
dobjFor(Examine)
{
verify()
{
/*
* When we're in the room, downgrade the likelihood a bit, as
* we'd rather inspect an object within the room if there's
* something with the same name. When we're *not* in the
* room - meaning this is a remote room that's visible from
* the player's location - downgrade the likelihood even
* more, since we're more likely to want to examine the room
* we're actually in than a remote room with the same name.
*/
if (gActor.isIn(self))
logicalRank(80, 'x room');
else
logicalRank(70, 'x room');
}
action()
{
/*
* if the looker is within the room, replace EXAMINE <self>
* with LOOK AROUND; otherwise, use the normal description
*/
if (gActor.isIn(self) && gActor.canSee(self))
gActor.lookAround(LookRoomDesc | LookListSpecials
| LookListPortables);
else
inherited();
}
}
/* treat LOOK IN <room> as EXAMINE <room> */
dobjFor(LookIn) remapTo(Examine, self)
/* LOOK UNDER and BEHIND are illogical */
dobjFor(LookUnder) { verify { illogical(&cannotLookUnderMsg); } }
dobjFor(LookBehind) { verify { illogical(&cannotLookBehindMsg); } }
/* treat SMELL/LISTEN TO <room> as just SMELL/LISTEN */
dobjFor(Smell) remapTo(SmellImplicit)
dobjFor(ListenTo) remapTo(ListenImplicit)
/* map STAND/SIT/LIE ON <room> to my default floor */
dobjFor(StandOn) maybeRemapTo(roomFloor != nil, StandOn, roomFloor)
dobjFor(SitOn) maybeRemapTo(roomFloor != nil, SitOn, roomFloor)
dobjFor(LieOn) maybeRemapTo(roomFloor != nil, LieOn, roomFloor)
/*
* treat an explicit GET OUT OF <ROOM> as OUT if there's an apparent
* destination for OUT; otherwise treat it as "vague travel," which
* simply tells the player that they need to specify a direction
*/
dobjFor(GetOutOf)
{
remap()
{
/* remap only if this isn't an implied action */
if (gAction.parentAction == nil)
{
/*
* if we have an apparent Out connection, go there;
* otherwise it's not obvious where we're meant to go
*/
if (out != nil && out.isConnectorApparent(self, gActor))
return [OutAction];
else
return [VagueTravelAction];
}
/* don't remap here - use the standard handling */
return inherited();
}
}
/*
* for BOARD and ENTER, there are three possibilities:
*
* - we're already directly in this room, in which case it's
* illogical to travel here again
*
* - we're in a nested room within this room, in which case ENTER
* <self> is the same as GET OUT OF <outermost nested room within
* self>
*
* - we're in a separate top-level room that's connected by a sense
* connector, in which case ENTER <self> should be handled as TRAVEL
* VIA <connector from actor's current location to self>
*/
dobjFor(Board) asDobjFor(Enter)
dobjFor(Enter)
{
verify()
{
/*
* if we're already here, entering the same location is
* redundant; if we're not in a nested room, we need a travel
* connector to get there from here
*/
if (gActor.isDirectlyIn(self))
illogicalAlready(&alreadyInLocMsg);
else if (!gActor.isIn(self)
&& gActor.location.getConnectorTo(gActor, self) == nil)
illogicalNow(&whereToGoMsg);
}
preCond()
{
/*
* if we're in a different top-level room, and there's a
* travel connector, we'll simply travel via the connector,
* so we don't need to impose any pre-conditions of our own
*/
if (!gActor.isIn(self)
&& gActor.location.getConnectorTo(gActor, self) != nil)
return [];
/*
* if we're in a nested room within this object, we'll
* replace the action with "get out of <outermost nested
* room>", so there's no need for any extra preconditions
* here
*/
if (gActor.isIn(self) && !gActor.isDirectlyIn(self))
return [];
/* otherwise, use the default conditions */
return inherited();
}
action()
{
/*
* if we're in a nested room, get out; otherwise travel via a
* suitable travel connector
*/
if (gActor.isIn(self) && !gActor.isDirectlyIn(self))
{
/*
* get out of the *outermost* nested room - that is, our
* direct child that contains the actor
*/
local chi = contents.valWhich({x: gActor.isIn(x)});
/* if we found it, get out of it */
if (chi != nil)
replaceAction(GetOutOf, chi);
}
else
{
/* get the connector from here to there */
local conn = gActor.location.getConnectorTo(gActor, self);
/* if we found it, go that way */
if (conn != nil)
replaceAction(TravelVia, conn);
}
/*
* if we didn't replace the action yet, we can't figure out
* how to get here from the actor's current location
*/
reportFailure(&whereToGoMsg);
}
}
;
/*
* A dark room, which provides no light of its own
*/
class DarkRoom: Room
/*
* turn off the lights
*/
brightness = 0
;
/*
* An outdoor location. This differs from an indoor location in that it
* has ground and sky rather than floor and ceiling, and has no walls.
*/
class OutdoorRoom: Room
/* an outdoor room has ground and sky, but no walls */
roomParts = [defaultGround, defaultSky]
;
/*
* A shipboard room. This is a simple mix-in class: it can be used
* along with any type of Room to indicate that this room is aboard a
* ship. When a room is aboard a ship, the shipboard travel directions
* (port, starboard, fore, aft) are allowed; these directions normally
* make no sense.
*
* This is a mix-in class rather than a Room subclass to allow it to be
* used in conjunction with any other Room subclass. To make a room
* shipboard, simply declare your room like this:
*
* mainDeck: Shipboard, Room // etc
*/
class Shipboard: object
/* mark the location as being aboard ship */
isShipboard = true
;
/*
* For convenience, we define ShipboardRoom as a shipboard version of the
* basic Room type.
*/
class ShipboardRoom: Shipboard, Room
;
/* ------------------------------------------------------------------------ */
/*
* Make a room "floorless." This is a mix-in class that you can include
* in a superclass list ahead of Room or any of its subclasses to create
* a room where support is provided by some means other than standing on
* a surface, or where there's simply no support. Examples: hanging on a
* rope over a chasm; climbing a ladder; in free-fall after jumping out
* of a plane; levitating in mid-air.
*
* There are two main special features of a floorless room. First, and
* most obviously, there's no "floor" or "ground" object among the room
* parts. We accomplish this by simply subtracting out any object of
* class Floor from the room parts list inherited from the combined base
* room class.
*
* Second, there's no place to put anything down, so objects dropped here
* either disappear from the game or are transported to another location
* (the room at the bottom of the chasm, for example).
*/
class Floorless: object
/*
* Omit the default floor/ground objects from the room parts list.
* Room classes generally have static room parts lists, so calculate
* this once per instance and store the results.
*
* NOTE - if you combine Floorless with a base Room class that has a
* dynamic room parts list, you'll need to override this to calculate
* the subset dynamically on each invocation.
*/
roomParts = perInstance(inherited().subset({x: !x.ofKind(Floor)}))
/*
* The room below, if any - this is where objects dropped here will
* actually end up. By default, this is nil, which means that
* objects dropped here simply disappear from the game. If there's a
* "bottom of chasm" location where dropped objects should land,
* provide it here.
*/
bottomRoom = nil
/* receive a dropped object */
receiveDrop(obj, desc)
{
/*
* move the dropped object to the room at the bottom of whatever
* it is we're suspended over; if there is no bottom room, we'll
* simply remove the dropped object from the game
*/
obj.moveInto(bottomRoom);
/*
* Say that the object drops out of sight below. Build this by
* combining the generic report prefix from the drop descriptor
* with our generic suffix, which describes the object as
* vanishing below.
*/
mainReport(desc.getReportPrefix(obj, self)
+ gActor.getActionMessageObj().floorlessDropMsg(obj));
}
;
/*
* For convenience, provide a combination of Floorless with the ordinary
* Room.
*/
class FloorlessRoom: Floorless, Room;
/* ------------------------------------------------------------------------ */
/*
* Room Part - base class for "parts" of rooms, such as floors and walls.
* Room parts are unusual in a couple of ways.
*
* First, room parts are frequently re-used widely throughout a game. We
* define a single instance of each of several parts that are found in
* typical rooms, and then re-use those instances in all rooms with those
* parts. For example, we define one "default floor" object, and then
* use that object in most or all rooms that have floors. We do this for
* efficiency, to avoid creating hundreds of essentially identical copies
* of the common parts.
*
* Second, because room parts are designed to be re-used, things that are
* in or on the parts are actually represented in the containment model
* as being directly in their containing rooms. For example, an object
* that is said to be "on the floor" actually has its 'location' property
* set to its immediate room container, and the 'contents' list that
* contains the object is that of the room, not of the floor object. We
* must therefore override some of the normal handling for object
* locations within room parts, in order to make it appear (for the
* purposes of command input and descriptive messages) that the things
* are in/on their room parts, even though they're not really represented
* that way in the containment model.
*/
class RoomPart: Fixture
/*
* When we explicitly examine a RoomPart, list any object that's
* nominally contained in the room part, as long it doesn't have a
* special description for the purposes of the room part. (If it
* does have a special description, then examining the room part will
* automatically display that special desc, so we don't want to
* include the object in a separate list of miscellaneous contents of
* the room part.)
*/
isObjListedInRoomPart(obj)
{
/*
* list the object *unless* it has a special description for the
* purposes of examining this room part
*/
return !obj.useSpecialDescInRoomPart(self);
}
/*
* Add this room part to the given room.
*
* Room parts don't have normal "location" properties. Instead, a
* room part explicitly appears in the "roomParts" list of each room
* that contains it. For the most part, room parts are static -
* they're initialized in the room definitions and never changed.
* However, if you need to dynamically add a room part to a room
* during the game, you can do so using this method.
*/
moveIntoAdd(room)
{
/* add me to the room's 'roomParts' and 'contents' lists */
room.roomParts += self;
room.contents += self;
}
/*
* Remove this room part from the given room. This can be used if
* it's necessary to remove the room part dynamically from a room.
*/
moveOutOf(room)
{
/* remove me from the room's 'roomParts' and 'contents' lists */
room.roomParts -= self;
room.contents -= self;
}
/*
* Don't include room parts in 'all'. Room parts are so ubiquitous
* that we never want to assume that they're involved in a command
* except when it is specifically so stated.
*/
hideFromAll(action) { return true; }
/* do allow use as a default, though */
hideFromDefault(action) { return nil; }
/*
* When multiple room parts show up in a resolve list, and some of
* the parts are local to the actor's immediate location and others
* aren't, keep only the local ones. This helps avoid pointless
* ambiguity in cases where two (or more) top-level locations are
* linked with a sense connector, and one or the other location has
* custom room part objects.
*/
filterResolveList(lst, action, whichObj, np, requiredNum)
{
/* if a definite number of objects is required, check ambiguity */
if (requiredNum != nil)
{
/* get the subset that's just RoomParts */
local partLst = lst.subset({x: x.obj_.ofKind(RoomPart)});
/*
* get the *remote* subset - this is the subset that's not in
* the outermost room of the target actor
*/
local outer = action.actor_.getOutermostRoom();
local remoteLst = partLst.subset({x: !x.obj_.isIn(outer)});
/*
* If all of the objects are remote, or all of them are
* local, we can't narrow things down on this basis; but if
* we found some remote and some local, eliminate the remote
* items, since we want to favor the local ones
*/
if (remoteLst.length() not in (0, partLst.length()))
lst -= remoteLst;
}
/* now do any inherited work, and return the result */
return inherited(lst, action, whichObj, np, requiredNum);
}
/*
* Since room parts are generally things like walls and floors that
* enclose the entire room, they're typically visually large, and
* tend to have fairly large-scale details (such as doors and
* windows). So, by default we set the sightSize to 'large' so that
* the details are visible at a distance.
*/
sightSize = large
/*
* as with decorations, downgrade the likelihood for Examine, as the
* standard walls, floors, etc. are pretty much background noise
* that are just here in case someone wants to refer to them
* explicitly
*/
dobjFor(Examine)
{
verify()
{
inherited();
logicalRank(70, 'x decoration');
}
}
/* describe the status - shows the things that are in/on the part */
examineStatus()
{
/* show the contents of the room part */
examinePartContents(&descContentsLister);
}
/* show our contents */
examinePartContents(listerProp)
{
/*
* Get my location, as perceived by the actor - this is the room
* that contains this part. If I don't have a location as
* perceived by the actor, then we can't show any contents.
*/
local loc = gActor.location.getRoomPartLocation(self);
if (loc == nil)
return;
/*
* create a copy of the lister customized for this part, if we
* haven't already done so
*/
if (self.(listerProp).part_ == nil)
{
self.(listerProp) = self.(listerProp).createClone();
self.(listerProp).part_ = self;
}
/* show the contents of the containing location */
self.(listerProp).showList(gActor, self, loc.contents, 0, 0,
gActor.visibleInfoTable(), nil);
}
/*
* show our special contents - this shows objects with special
* descriptions that are specifically in this room part
*/
examineSpecialContents()
{
local infoTab;
local lst;
/* get the actor's list of visible items */
infoTab = gActor.visibleInfoTable();
/*
* get the list of special description items, using only the
* subset that uses special descriptions in this room part
*/
lst = specialDescList(infoTab,
{obj: obj.useSpecialDescInRoomPart(self)});
/* show the list */
specialContentsLister.showList(gActor, nil, lst, 0, 0, infoTab, nil);
}
/*
* Get the destination for a thrown object that hits me. Since we
* don't have a real location, we must ask the actor for our room
* part location, and then use its hit-and-fall destination.
*/
getHitFallDestination(thrownObj, path)
{
local loc;
local dest;
/*
* if we have an explicit location, start with it; otherwise, ask
* the actor's location to find us; if we can't even find
* ourselves there, just use the actor's current location
*/
if ((loc = location) == nil
&& (loc = gActor.location.getRoomPartLocation(self)) == nil)
loc = gActor.location;
/* use the location's drop destination for thrown objects */
dest = loc.getDropDestination(thrownObj, path);
/* give the destination a chance to make adjustments */
return dest.adjustThrowDestination(thrownObj, path);
}
/* consider me to be in any room of which I'm a part */
isIn(loc)
{
local rpl;
/*
* get the room-part location of this room part, from the
* perspective of the prospective location we're asking about
*/
if (loc != nil && (rpl = loc.getRoomPartLocation(self)) != nil)
{
/*
* We indeed have a room part location in the given
* location, so we're a part of the overall location. We
* might be directly in the location (i.e., 'rpl' could
* equal 'loc'), or we might be somewhere relative to 'loc'
* (for example, we could be within a nested room within
* 'loc', in which case 'rpl' is a nested room unequal to
* 'loc' but is within 'loc'). So, if 'rpl' equals 'loc' or
* is contained in 'loc', I'm within 'loc', because I'm
* within 'rpl'.
*/
if (rpl == loc || rpl.isIn(loc))
return true;
}
/*
* we don't appear to be in 'loc' on the basis of our special
* room-part location; fall back on the inherited handling
*/
return inherited(loc);
}
/* our contents listers */
contentsLister = roomPartContentsLister
descContentsLister = roomPartDescContentsLister
lookInLister = roomPartLookInLister
specialContentsLister = specialDescLister
/* look in/on: show our contents */
dobjFor(LookIn)
{
verify() { }
action()
{
/* show my contents */
examinePartContents(&lookInLister);
/* show my special contents */
examineSpecialContents();
}
}
/* we can't look behind/through/under a room part by default */
nothingUnderMsg = &cannotLookUnderMsg
nothingBehindMsg = &cannotLookBehindMsg
nothingThroughMsg = &cannotLookThroughMsg
/*
* initialization - add myself to my location's roomPart list if I
* have an explicit location
*/
initializeThing()
{
/* do the normal work first */
inherited();
/*
* if I have an explicit location, and I'm not in my location's
* roomPart list, add myself to the list
*/
if (location != nil && location.roomParts.indexOf(self) == nil)
location.roomParts += self;
}
;
/*
* A floor for a nested room. This should be placed directly within a
* nested room object if the nested room is to be described as having a
* floor separate from the nested room itself. We simply remap any
* commands relating to using the floor as a surface (Put On, Throw At,
* Sit On, Lie On, Stand On) to the enclosing nested room.
*/
class NestedRoomFloor: Fixture
iobjFor(PutOn) remapTo(PutOn, DirectObject, location)
iobjFor(ThrowAt) remapTo(ThrowAt, DirectObject, location)
dobjFor(SitOn) remapTo(SitOn, location)
dobjFor(LieOn) remapTo(LieOn, location)
dobjFor(StandOn) remapTo(StandOn, location)
;
/*
* Base class for the default floor and the default ground of a top-level
* room. The floor and ground are where things usually go when dropped,
* and they're the locations where actors within a room are normally
* standing.
*/
class Floor: RoomPart
/* specifically allow me as a default for STAND ON, SIT ON, and LIE ON */
hideFromDefault(action)
{
/* don't hide from STAND ON, SIT ON, LIE ON */
if (action.ofKind(StandOnAction)
|| action.ofKind(SitOnAction)
|| action.ofKind(LieOnAction))
return nil;
/* for other actions, use the standard handling */
return inherited(action);
}
/*
* When explicitly examining a Floor object, list any objects that
* are listed in the normal room description (as in LOOK AROUND). By
* default, the floor is the nominal container for anything directly
* in the room, so we'll normally want LOOK AROUND and LOOK AT FLOOR
* to produce the same list of objects.
*/
isObjListedInRoomPart(obj)
{
/* list the object if it's listed in a normal LOOK AROUND */
return obj.isListed;
}
/*
* 'put x on floor' equals 'drop x'. Add a precondition that the
* drop destination is the main room, since otherwise we could have
* strange results if we dropped something inside a nested room.
*/
iobjFor(PutOn)
{
preCond()
{
/*
* require that this floor object itself is reachable, and
* that the drop destination for the direct object is an
* outermost room
*/
return [touchObj, new ObjectPreCondition(
gDobj, dropDestinationIsOuterRoom)];
}
verify() { }
action() { replaceAction(Drop, gDobj); }
}
/*
* The message we use to describe this object prepositionally, as the
* destination of a throw or drop. This should be a gLibMessages
* property with the appropriate prepositional phrase. We use a
* custom message specific to floor-like objects.
*/
putDestMessage = &putDestFloor
/* 'throw x at floor' */
iobjFor(ThrowAt)
{
check()
{
/*
* If I'm reachable, suggest just putting it down instead.
* We only make the suggestion, rather than automatically
* treating the command as DROP, because a player explicitly
* typing THROW <obj> AT FLOOR is probably attempting to
* express something more violent than merely putting the
* object down; the player probably is thinking in terms of
* breaking the object (or the floor).
*/
if (canBeTouchedBy(gActor))
{
mainReport(&shouldNotThrowAtFloorMsg);
exit;
}
}
}
/* is the given actor already on the floor? */
isActorOnFloor(actor)
{
/*
* the actor is on the floor if the actor is directly in the
* floor's room-part-location for the actor's location
*/
return actor.isDirectlyIn(actor.location.getRoomPartLocation(self));
}
/* verify sitting/standing/lying on the floor */
verifyEntry(newPosture, alreadyMsg)
{
/*
* If we're already in my location, and we're in the desired
* posture, this command is illogical because we're already
* where they want us to end up. Otherwise, it's logical to
* stand/sit/lie on the floor, but rank it low since we don't
* want the floor to interfere with selecting a default if
* there's anything around that's actually like a chair.
*
* If it's logical, note that we've verified okay for the
* action. On a future pass, we might have enforced a
* precondition that moved us here, at which point our work will
* be done - but we don't want to complain in that case, since
* it was logical from the player's perspective to carry out the
* command even though we have nothing left to do.
*/
if (gActor.posture == newPosture
&& isActorOnFloor(gActor)
&& gAction.verifiedOkay.indexOf(self) == nil)
{
/* we're already on the floor in the desired posture */
illogicalNow(alreadyMsg);
}
else
{
/*
* it's logical, but rank it low in case there's something
* more special we can stand/sit/lie on
*/
logicalRank(50, 'on floor');
/*
* note that we've verified okay, so we don't complain on a
* future pass if we discover that a precondition has
* brought us into compliance with the request prematurely
*/
gAction.verifiedOkay += self;
}
}
/* perform sitting/standing/lying on the floor */
performEntry(newPosture)
{
/*
* bring the new posture into effect; there's no need for
* actually moving the actor, since the preconditions will have
* moved us to our main enclosing room already
*/
gActor.makePosture(newPosture);
/* report success */
defaultReport(&roomOkayPostureChangeMsg, newPosture, self);
}
/* 'stand on floor' causes actor to stand in the containing room */
dobjFor(StandOn)
{
preCond = [touchObj,
new ObjectPreCondition(gActor.location.getOutermostRoom(),
actorDirectlyInRoom)]
verify() { verifyEntry(standing, &alreadyStandingOnMsg); }
action() { performEntry(standing); }
}
/* 'sit on floor' causes the actor to sit in the containing room */
dobjFor(SitOn)
{
preCond = [touchObj,
new ObjectPreCondition(gActor.location.getOutermostRoom(),
actorDirectlyInRoom)]
verify() { verifyEntry(sitting, &alreadySittingOnMsg); }
action() { performEntry(sitting); }
}
/* 'lie on floor' causes the actor to lie down in the room */
dobjFor(LieOn)
{
preCond = [touchObj,
new ObjectPreCondition(gActor.location.getOutermostRoom(),
actorDirectlyInRoom)]
verify() { verifyEntry(lying, &alreadyLyingOnMsg); }
action() { performEntry(lying); }
}
/*
* Mention that an actor is here, as part of a room description.
* When the actor is standing, just say that the actor is here, since
* it's overstating the obvious to say that the actor is standing on
* the floor. For other postures, do mention the floor.
*/
roomActorHereDesc(actor)
{
/*
* if we're standing, just say that the actor is "here";
* otherwise, say that the actor is sitting/lying/etc on self
*/
if (actor.posture == standing)
gLibMessages.roomActorHereDesc(actor);
else
gLibMessages.actorInRoom(actor, self);
}
/*
* Mention that an actor is here, as part of a room description.
* Since a floor is a trivial part of its enclosing room, there's no
* point in mentioning that we're on the floor, as that's stating the
* obvious; instead, simply describe the actor as being in the
* actor's actual enclosing room.
*/
roomActorThereDesc(actor) { actor.location.roomActorThereDesc(actor); }
/*
* Show our room name status for an actor on the floor. Since
* standing on the floor is the trivial default for any room, we
* won't bother mentioning it. Other postures we'll mention the same
* way we would for any nested room.
*/
roomActorStatus(actor)
{
if (actor.posture != standing)
gLibMessages.actorInRoomStatus(actor, self);
}
/*
* Show the actor's posture here. When we're standing on the floor,
* don't mention the posture, as this is too trivial a condition to
* state. Otherwise, mention it as normal for a nested room.
*/
roomActorPostureDesc(actor)
{
if (actor.posture != standing)
gLibMessages.actorInRoomPosture(actor, self);
}
/*
* Generate an acknowledgment for a posture change here. If the
* actor is standing, just say "okay, you're now standing" without
* mentioning the floor, since standing on the floor is the trivial
* default. For other postures, say that we're sitting/lying/etc on
* the floor.
*/
roomOkayPostureChange(actor)
{
if (actor.posture == standing)
defaultReport(&okayPostureChangeMsg, standing);
else
defaultReport(&roomOkayPostureChangeMsg, actor.posture, self);
}
/*
* mention the actor as part of the EXAMINE description of a nested
* room containing the actor
*/
roomListActorPosture(actor)
{
/*
* Since standing is the default posture for an actor, and since
* the floor (or equivalent) is the default place to be standing,
* don't bother mentioning actors standing on a floor.
* Otherwise, mention that the actor is sitting/lying/etc here.
*/
if (actor.posture != standing)
gLibMessages.actorInRoom(actor, self);
}
/*
* Prefix and suffix messages for listing a group of actors
* nominally on the this floor. Actors are said to be on the floor
* when they're really in the location containing the floor.
*
* If we're talking about a remote location, simply describe it as
* the location rather than mentioning the floor, since the floor is
* a trivial part of the remote location not worth mentioning.
*
* If we're local, and we're standing, we'll simply say that we're
* "standing here"; again, saying that we're standing on the floor
* is stating the obvious. If we're not standing, we will mention
* that we're on the floor.
*/
actorInGroupPrefix(pov, posture, remote, lst)
{
if (remote != nil)
gLibMessages.actorThereGroupPrefix(pov, posture, remote, lst);
else if (posture == standing)
gLibMessages.actorHereGroupPrefix(posture, lst);
else
gLibMessages.actorInGroupPrefix(posture, self, lst);
}
actorInGroupSuffix(pov, posture, remote, lst)
{
if (remote != nil)
gLibMessages.actorThereGroupSuffix(pov, posture, remote, lst);
else if (posture == standing)
gLibMessages.actorHereGroupSuffix(posture, lst);
else
gLibMessages.actorInGroupSuffix(posture, self, lst);
}
;
/* ------------------------------------------------------------------------ */
/*
* Define the default room parts.
*/
/*
* the default floor, for indoor locations
*/
defaultFloor: Floor
;
/*
* the default ceiling, for indoor locations
*/
defaultCeiling: RoomPart
;
/*
* The default walls, for indoor locations. We provide a north, south,
* east, and west wall in each indoor location by default.
*/
class DefaultWall: RoomPart
;
defaultNorthWall: DefaultWall
;
defaultSouthWall: DefaultWall
;
defaultEastWall: DefaultWall
;
defaultWestWall: DefaultWall
;
/*
* the default sky, for outdoor locations
*/
defaultSky: Distant, RoomPart
;
/*
* The default ground, for outdoor locations.
*/
defaultGround: Floor
;
/* ------------------------------------------------------------------------ */
/*
* A "room part item" is an object that's specially described as being
* part of, or attached to, a RoomPart (a wall, ceiling, floor, or the
* like). This is a mix-in class that can be combined with any ordinary
* object class (but usually with something non-portable, such as a
* Fixture or Immovable). The effect of adding RoomPartItem to an
* object's superclasses is that a command like EXAMINE EAST WALL (or
* whichever room part the object is associated with) will display the
* object's specialDesc, but a simple LOOK will not. This class is
* sometimes useful for things like doors, windows, ceiling fans, and
* other things attached to the room.
*
* Note that this is a mix-in class, so you should always combine it with
* a regular Thing-based class.
*
* When using this class, you should define two properties in the object:
* specialNominalRoomPartLocation, which you should set to the RoomPart
* (such as a wall) where the object should be described; and
* specialDesc, which is the description to show when the room part is
* examined. Alternatively (or in addition), you can define
* initNominalRoomPartLocation and initSpecialDesc - these work the same
* way, but will only be in effect until the object is moved.
*/
class RoomPartItem: object
/*
* show our special description when examining our associated room
* part, as long as we actually define a special description
*/
useSpecialDescInRoomPart(part)
{
/* only show the special description in our associated room part */
if (!isNominallyInRoomPart(part))
return nil;
/*
* if we define an initial special description, and this is our
* nominal room part for that description, use it
*/
if (isInInitState
&& propType(&initSpecialDesc) != TypeNil
&& initNominalRoomPartLocation == part)
return true;
/* likewise for our specialDesc */
if (propType(&specialDesc) != TypeNil
&& specialNominalRoomPartLocation == part)
return true;
/* otherwise, don't use the special description */
return nil;
}
/*
* don't use the special description in room descriptions, or in
* examining any other container
*/
useSpecialDescInRoom(room) { return nil; }
useSpecialDescInContents(cont) { return nil; }
;
/* ------------------------------------------------------------------------ */
/*
* A Nested Room is any object that isn't a room but which can contain
* an actor: chairs, beds, platforms, vehicles, and the like.
*
* An important property of nested rooms is that they're not
* full-fledged rooms for the purposes of actor arrivals and departures.
* Specifically, an actor moving from a room to a nested room within the
* room does not trigger an actor.travelTo invocation, but simply moves
* the actor from the containing room to the nested room. Moving from
* the nested room to the containing room likewise triggers no
* actor.travelTo invocation. The travelTo method is not applicable for
* intra-room travel because no TravelConnector objects are traversed in
* such travel; we simply move in and out of contained objects. To
* mitigate this loss of notification, we instead call
* actor.travelWithin() when moving among nested locations.
*
* By default, an actor attempting to travel from a nested location via
* a directional command will simply attempt the travel as though the
* actor were in the enclosing location.
*/
class NestedRoom: BasicLocation
/*
* Our interior room name. This is the status line name we display
* when an actor is within this object and can't see out to the
* enclosing room. Since we can't rely on the enclosing room's
* status line name if we can't see the enclosing room, we must
* provide one of our own.
*
* By default, we'll use our regular name.
*/
roomName = (name)
/*
* Show our interior room description. We use this to generate the
* long "look" description for the room when an actor is within the
* room and cannot see the enclosing room.
*
* Note that this is used ONLY when the actor cannot see the
* enclosing room - when the enclosing room is visible (because the
* nested room is something like a chair that doesn't enclose the
* actor, or can enclose the actor but is open or transparent), then
* we'll simply use the description of the enclosing room instead,
* adding a note to the short name shown at the start of the room
* description indicating that the actor is in the nested room.
*
* By default, we'll show the appropriate "actor here" description
* for the posture, so we'll say something like "You are sitting on
* the red chair" or "You are in the phone booth." Instances can
* override this to customize the description with something more
* detailed, if desired.
*/
roomDesc
{
local pov = getPOVActorDefault(gActor);
pov.listActorPosture(pov);
}
/*
* The maximum bulk the room can hold. We'll define this to a large
* number by default so that bulk isn't a concern.
*
* Lower numbers here can be used, for example, to limit the seating
* capacity of a chair.
*/
bulkCapacity = 10000
/*
* Check for ownership. For a nested room, an actor can be taken to
* own the nested room by virtue of being inside the room - the
* chair Bob is sitting in can be called "bob's chair".
*
* If we don't have an explicit owner, and the potential owner 'obj'
* is in me and can own me, we'll report that 'obj' does in fact own
* me. Otherwise, we'll defer to the inherited implementation.
*/
isOwnedBy(obj)
{
/*
* if we're not explicitly owned, and 'obj' can own me, and
* 'obj' is inside me, consider us owned by 'obj'
*/
if (owner == nil && obj.isIn(self) && obj.canOwn(self))
return true;
/* defer to the inherited definition of ownership */
return inherited(obj);
}
/*
* Get the extra scope items within this room. Normally, the
* immediately enclosing nested room is in scope for an actor in the
* room. So, if the actor is directly in 'self', return 'self'.
*/
getExtraScopeItems(actor)
{
/* if the actor is directly in the nested room, return it */
if (actor.isDirectlyIn(self))
return [self];
else
return [];
}
/*
* By default, 'out' within a nested room should take us out of the
* nested room itself. The easy way to accomplish this is to set up
* a 'nestedRoomOut' connector for the nested room, which will
* automatically try a GET OUT command. If we didn't do this, we'd
* *usually* pick up the noTravelOut from our enclosing room, but
* only when the enclosing room didn't override 'out' to point
* somewhere else. Explicitly setting up a 'noTravelOut' here
* ensures that we'll consistently GET OUT of the nested room even if
* the enclosing room has its own 'out' destination.
*
* Note that nestedRoomOut shows as a listed exit in exit listings
* (for the EXITS command and in the status line). If you don't want
* OUT to be listed as an available exit for the nested room, you
* should override this to use noTravelOut instead.
*/
out = nestedRoomOut
/*
* An actor is attempting to "get out" while in this location. By
* default, we'll treat this as getting out of this object. This
* can be overridden if "get out" should do something different.
*/
disembarkRoom()
{
/* run the appropriate command to get out of this nested room */
removeFromNested();
}
/*
* Make the actor stand up from this location. By default, we'll
* cause the actor to travel (using travelWithin) to our container,
* and assume the appropriate posture for the container.
*/
makeStandingUp()
{
/* remember the old posture, in case the travel fails */
local oldPosture = gActor.posture;
/* get the exit destination; if there isn't one, we can't proceed */
local dest = exitDestination;
if (dest == nil)
{
reportFailure(&cannotDoFromHereMsg);
exit;
}
/*
* Set the actor's posture to the default for the destination.
* Do this before effecting the actual travel, so that the
* destination can change this default if it wants.
*/
gActor.makePosture(dest.defaultPosture);
/* protect against 'exit' and the like during the travel attempt */
try
{
/*
* move the actor to our exit destination, traveling entirely
* within nested locations
*/
gActor.travelWithin(dest);
}
finally
{
/* if we didn't end up traveling, restore the old posture */
if (gActor.isIn(self))
gActor.makePosture(oldPosture);
}
/* generate the appropriate default for the new location */
gActor.okayPostureChange();
}
/*
* Try an implied command to move the actor from outside of this
* nested room into this nested room. This must be overridden in
* subclasses to carry out the appropriate implied command. Returns
* the result of tryImplicitAction().
*
* This is called when we need to move an actor into this location
* as part of an implied command. We use an overridable method
* because different kinds of nested rooms have different commands
* for entering: SIT ON CHAIR, LIE ON BED, GET IN CAR, RIDE BIKE,
* and so on. This should be normally be overridden imply by
* calling tryImplicitAction() with the appropriate command for the
* specific type of nested room, and returning the result.
*/
tryMovingIntoNested()
{
/* do nothing by default - subclasses must override */
return nil;
}
/*
* message property to use for reportFailure when
* tryMovingIntoNested fails
*/
mustMoveIntoProp = nil
/*
* Try an implied command to remove an actor from this location and
* place the actor in my immediate containing location. This must
* be overridden in subclasses to carry out the appropriate implied
* command. Returns the result of tryImplicitAction().
*
* This is essentially the reverse of tryMovingIntoNested(), and
* should in most cases be implemented by calling
* tryImplicitAction() with the appropriate command to get out of
* the room, and returning the result.
*/
tryRemovingFromNested()
{
/* do nothing by default - subclasses must override */
return nil;
}
/*
* Replace the current action with one that removes the actor from
* this nested room. This is used to implement the GET OUT command
* when the actor is directly in this nested room. In most cases,
* this should simply be implemented with a call to replaceAction()
* with the appropriate command.
*/
removeFromNested()
{
/* subclasses must override */
}
/*
* Try moving the actor into this location. This is used to move
* the actor into this location as part of meeting preconditions,
* and we use the normal precondition check protocol: we return nil
* if the condition (actor is in this room) is already met; we
* return true if we successfully execute an implied command to meet
* the condition; and we report a failure message and terminate the
* command with 'exit' if we don't know how to meet the condition or
* the implied command we try to execute fails or fails to satisfy
* the condition.
*
* This does not normally need to be overridden in subclasses.
*/
checkMovingActorInto(allowImplicit)
{
/* if the actor is within me, use default handling */
if (gActor.isIn(self))
return inherited(allowImplicit);
/* try an implied command to move the actor into this nested room */
if (allowImplicit && tryMovingIntoNested())
{
/* if we didn't succeed, terminate the command */
if (!gActor.isDirectlyIn(self))
exit;
/* tell the caller we executed an implied command */
return true;
}
/*
* if we can be seen, report that the actor must travel here
* first; if we can't be seen, simply say that this can't be done
* from here
*/
if (gActor.canSee(self))
{
/* report that we have to move into 'self' first */
reportFailure(mustMoveIntoProp, self);
}
else
{
/*
* we can't be seen; simply say this we can't do this command
* from the current location
*/
reportFailure(&cannotDoFromHereMsg);
}
/* terminate the action */
exit;
}
/*
* Check, using pre-condition rules, that the actor is removed from
* this nested location and moved to my immediate location. This is
* used to enforce a precondition that the actor is in the enclosing
* location.
*
* This isn't normally overridden in subclasses.
*/
checkActorOutOfNested(allowImplicit)
{
/* try removing the actor from this nested location */
if (allowImplicit && tryRemovingFromNested())
{
/*
* make sure we managed to move the actor to our exit
* destination
*/
if (!gActor.isDirectlyIn(exitDestination))
exit;
/* indicate that we carried out an implied command */
return true;
}
/* we can't carry out our implied departure plan - fail */
reportFailure(&cannotDoFromMsg, self);
exit;
}
/*
* Check, using pre-condition rules, that the actor is ready to
* enter this room as a nested location.
*
* This isn't normally overridden in subclasses.
*/
checkActorReadyToEnterNestedRoom(allowImplicit)
{
/*
* If the actor is directly in this room, we obviously need do
* nothing, as the actor is already in this nested room.
*/
if (gActor.isDirectlyIn(self))
return nil;
/*
* If the actor isn't within us (directly or indirectly), we
* must move the actor to a valid "staging location," so that
* the actor can move from the staging location into us. (A
* staging location is simply any location from which we can
* move directly into this nested room without any intervening
* travel in or out of other nested rooms.)
*/
if (!gActor.isIn(self))
return checkActorInStagingLocation(allowImplicit);
/*
* The actor is within us, but isn't directly within us, so
* handle this with the normal routine to move the actor into
* this room.
*/
return checkMovingActorInto(allowImplicit);
}
/*
* Check, using precondition rules, that the actor is in a valid
* "staging location" for entering this nested room. We'll ensure
* that the actor is directly in one of the locations in our
* stagingLocations list, running an appropriate implicit command to
* move the actor to the first item in that list if the actor isn't
* in any of them.
*
* This isn't normally overridden in subclasses.
*/
checkActorInStagingLocation(allowImplicit)
{
local lst;
local target;
/* get the list of staging locations */
lst = stagingLocations;
/* if there are no valid staging locations, we can't move here */
if (lst.length() == 0)
{
cannotMoveActorToStagingLocation();
exit;
}
/*
* Try each of the locations in our staging list, to see if the
* actor is directly in any of them.
*/
foreach (local cur in lst)
{
/*
* if the actor is directly in this staging location, then
* the actor can reach the destination with no additional
* intervening travel - simply return nil in this case to
* indicate that no implicit commands are needed before the
* proposed nested room entry
*/
if (gActor.isDirectlyIn(cur))
return nil;
}
/*
* The actor isn't directly in any staging location, so we must
* move the actor to an appropriate staging location before we
* can proceed. Choose a staging location based on the actor's
* current location.
*/
if ((target = chooseStagingLocation()) != nil)
{
/*
* We've chosen a target staging location. First, check to
* make sure the location we've chosen is valid - we might
* have chosen a default (such as the nested room's
* immediate container) that isn't usable as a staging
* location, so we need to check with it first to make sure
* it's willing to allow this.
*/
target.checkStagingLocation(self);
/*
* The check routine didn't abort the command, so try an
* appropriate implicit command to move the actor into the
* chosen staging location.
*/
return target.checkMovingActorInto(allowImplicit);
}
/*
* There's no apparent intermediate staging location given the
* actor's current location. We thus cannot proceed with the
* command; simply report that we can't get there from here.
*/
cannotMoveActorToStagingLocation();
exit;
}
/*
* Choose an intermediate staging location, given the actor's
* current location. This routine is called when the actor is
* attempting to move into 'self', but isn't in any of the allowed
* staging locations for 'self'; this routine's purpose is to choose
* the staging location that the actor should implicitly try to
* reach on the way to 'self'.
*
* By default, we'll attempt to find the first of our staging
* locations that indirectly contains the actor. (We know none of
* the staging locations directly contains the actor, because if one
* did, we wouldn't be called in the first place - we're only called
* when the actor isn't already directly in one of our staging
* locations.) This approach is appropriate when nested rooms are
* related purely by containment: if an actor is in a nested room
* within one of our staging locations, we can reach that staging
* location by having the actor get out of the more deeply nested
* room.
*
* However, this default approach is not appropriate when nested
* rooms are related in some way other than simple containment. We
* don't have any general framework for other types of nested room
* relationships, so this routine must be overridden in such a case
* with special-purpose code defining the special relationship.
*
* If we fail to find any staging location indirectly containing the
* actor, we'll return the result of defaultStagingLocation().
*/
chooseStagingLocation()
{
/* look for a staging location indirectly containing the actor */
foreach (local cur in stagingLocations)
{
/*
* if the actor is indirectly in this staging location,
* choose it as the target intermediate staging location
*/
if (gActor.isIn(cur))
return cur;
}
/*
* We didn't find any locations in the staging list that
* indirectly contain the actor, so use the default staging
* location.
*/
return defaultStagingLocation();
}
/*
* The default staging location for this nested room. This is the
* staging location we'll attempt to reach implicitly if the actor
* isn't in any of the rooms in the stagingLocations list already.
* We'll return the first element of our stagingLocations list for
* which isStagingLocationKnown returns true.
*/
defaultStagingLocation()
{
local lst;
/* get the list of valid staging locations */
lst = stagingLocations;
/* find the first element which is known to the actor */
foreach (local cur in lst)
{
/* if this staging location is known, take it as the default */
if (isStagingLocationKnown(cur))
return cur;
}
/* we didn't find any known staging locations - there's no default */
return nil;
}
/*
* Report that we are unable to move an actor to any staging
* location for this nested room. By default, we'll generate the
* message "you can't do that from here," but this can overridden to
* provide a more specific if desired.
*/
cannotMoveActorToStagingLocation()
{
/* report the standard "you can't do that from here" message */
reportFailure(&cannotDoFromHereMsg);
}
/*
* Report that we are unable to move an actor out of this nested
* room, because there's no valid 'exit destination'. This is
* called when we attempt to GET OUT OF the nested room, and the
* 'exitDestination' property is nil.
*/
cannotMoveActorOutOf()
{
/* report the standard "you can't do that from here" message */
reportFailure(&cannotDoFromHereMsg);
}
/*
* The valid "staging locations" for this nested room. This is a
* list of the rooms from which an actor can DIRECTLY reach this
* nested room; in other words, the actor will be allowed to enter
* 'self', with no intervening travel, if the actor is directly in
* any of these locations.
*
* If the list is empty, there are no valid staging locations.
*
* The point of listing staging locations is to make certain that
* the actor has to go through one of these locations in order to
* get into this nested room. This ensures that we enforce any
* conditions or trigger any side effects of moving through the
* staging locations, so that a player can't bypass a puzzle by
* trying to move directly from one location to another without
* going through the required intermediate steps. Since we always
* require that an actor go through one of our staging locations in
* order to enter this nested room, and since we carry out the
* travel to the staging location using implied commands (which are
* just ordinary commands, entered and executed automatically by the
* parser), we can avoid having to code any checks redudantly in
* both the staging locations and any other nearby locations.
*
* By default, an actor can only enter a nested room from the room's
* direct container. For example, if a chair is on a stage, an
* actor must be standing on the stage before the actor can sit on
* the chair.
*/
stagingLocations = [location]
/*
* Our exit destination. This is where an actor ends up when the
* actor is immediately inside this nested room and uses a "get out
* of" or equivalent command to exit the nested room.
*
* By default, we'll use the default staging location as the exit
* destination.
*/
exitDestination = (defaultStagingLocation())
/*
* Is the given staging location "known"? This returns true if the
* staging location is usable as a default, nil if not. If this
* returns true, then the location can be used in an implied command
* to move the actor to the staging location in order to move the
* actor into self.
*
* If this returns nil, no implied command will be attempted for
* this possible staging location. This doesn't mean that an actor
* gets a free pass through the staging location; on the contrary,
* it simply means that we won't try any automatic command to move
* an actor to the staging location, hence travel from a non-staging
* location to this nested room will simply fail. This can be used
* when part of the puzzle is to figure out that moving to the
* staging location is required in the first place: if we allowed an
* implied command in such cases, we'd give away the puzzle by
* solving it automatically.
*
* By default, we'll treat all of our staging locations as known.
*/
isStagingLocationKnown(loc) { return true; }
/*
* Get the travel preconditions for an actor in this location. By
* default, if we have a container, and the actor can see the
* container, we'll return its travel preconditions; otherwise, we'll
* use our inherited preconditions.
*/
roomTravelPreCond()
{
local ret;
/*
* If we can see out to our location, use the location's
* conditions, since by default we'll try traveling from the
* location; if we can't see out to our location, we won't be
* attempting travel through our location's connectors, so use
* our own preconditions instead.
*/
if (location != nil && gActor.canSee(location))
ret = location.roomTravelPreCond();
else
ret = inherited();
/* return the results */
return ret;
}
/*
* We cannot take a nested room that the actor is occupying
*/
dobjFor(Take)
{
verify()
{
/* it's illogical to take something that contains the actor */
if (gActor.isIn(self))
illogicalNow(&cannotTakeLocationMsg);
/* inherit the default handling */
inherited();
}
}
/*
* "get out of" action - exit the nested room
*/
dobjFor(GetOutOf)
{
preCond()
{
return [new ObjectPreCondition(self, actorDirectlyInRoom)];
}
verify()
{
/*
* the actor must be located on the platform; but allow the
* actor to be indirectly on the platform, since we'll use a
* precondition to move the actor out of any more nested
* rooms within us
*/
if (!gActor.isIn(self))
illogicalNow(¬OnPlatformMsg);
}
check()
{
/*
* If we have no 'exit destination' - that is, we have
* nowhere to go when we GET OUT OF the nested room - then
* prohibit the operation.
*/
if (exitDestination == nil)
{
/* explain the problem and terminate the command */
cannotMoveActorOutOf();
exit;
}
}
action()
{
/* travel to our get-out-of destination */
gActor.travelWithin(exitDestination);
/*
* set the actor's posture to the default posture for the
* new location
*/
gActor.makePosture(gActor.location.defaultPosture);
/* issue a default report of the change */
defaultReport(&okayNotStandingOnMsg);
}
}
/* explicitly define the push-travel indirect object mappings */
mapPushTravelIobj(PushTravelGetOutOf, TravelVia)
;
/* ------------------------------------------------------------------------ */
/*
* A "high nested room" is a nested room that is elevated above the rest
* of the room. This specializes the staging location handling so that
* it generates more specific messages.
*/
class HighNestedRoom: NestedRoom
/* report that we're unable to move to a staging location */
cannotMoveActorToStagingLocation()
{
reportFailure(&nestedRoomTooHighMsg, self);
}
/* if we can't get out, report that it's because we're too high up */
cannotMoveActorOutOf()
{
reportFailure(&nestedRoomTooHighToExitMsg, self);
}
/*
* Staging locations. By default, we'll return an empty list,
* because a high location is not usually reachable directly from its
* containing location.
*
* Note that puzzles involving moving platforms will have to manage
* this list dynamically, which could be done either by writing a
* method here that returns a list of currently valid staging
* locations, or by adding objects to this list as they become valid
* staging locations and removing them when they cease to be. For
* example, if we have an air vent in the ceiling that we can only
* reach when a chair is placed under the vent, this property could
* be implemented as a method that returns a list containing the
* chair only when the chair is in the under-the-vent state.
*
* Note that this empty default setting will also give us no exit
* destination, since the default exit location is the default
* staging location.
*/
stagingLocations = []
;
/* ------------------------------------------------------------------------ */
/*
* A chair is an item that an actor can sit on. When an actor is sitting
* on a chair, the chair contains the actor. In addition to sitting,
* chairs can optionally allow standing as well.
*
* We define the "BasicChair" as something that an actor can sit on, and
* then subclass this with the standard "Chair", which adds surface
* capabilities.
*/
class BasicChair: NestedRoom
/*
* A list of the allowed postures for this object. By default, we
* can sit and stand on a chair, since most ordinary chairs are
* suitable for both.
*/
allowedPostures = [sitting, standing]
/*
* A list of the obvious postures for this object. The only obvious,
* default thing you do with most ordinary chairs is sit on them,
* even they allow other postures. Something like a large sofa might
* want to allow both sitting and lying.
*
* This list differs from the allowed postures list because some
* postures might be possible but not probable. For most ordinary
* chairs, standing is possible, but it's not the first thing you'd
* think of doing with the chair.
*/
obviousPostures = [sitting]
/*
* A chair's effective follow location is usually its location's
* effective follow location, because we don't usually want to treat
* a chair as a separate location for the purposes of "follow."
* That is, if A and B are in the same room, and A sits down on a
* chair in the room, we don't want to count this as a move that B
* could follow.
*/
effectiveFollowLocation = (location.effectiveFollowLocation)
/*
* Try an implied command to move the actor from outside of this
* nested room into this nested room. By default, we'll call upon
* our default posture object to activate its command to move the
* actor into this object in the default posture. For a chair, the
* default posture is typically sitting, so the 'sitting' posture
* will perform a SIT ON <self> command.
*/
tryMovingIntoNested()
{
/*
* ask our default posture object to carry out the appropriate
* command to move the actor into 'self' in that posture
*/
return defaultPosture.tryMakingPosture(self);
}
/* tryMovingIntoNested failure message is "must sit on chair" */
mustMoveIntoProp = &mustSitOnMsg
/* default posture in this nested room is sitting */
defaultPosture = sitting
/*
* by default, objects dropped while sitting in a chair go into the
* enclosing location's drop destination
*/
getDropDestination(obj, path)
{
return location != nil
? location.getDropDestination(obj, path)
: self;
}
/*
* Remove an actor from the chair. By default, we'll simply stand
* up, since this is the normal way out of a chair.
*/
tryRemovingFromNested()
{
/* try standing up */
if (gActor.posture == sitting)
return tryImplicitAction(Stand);
else
return tryImplicitAction(GetOffOf, self);
}
/*
* Run the appropriate command to remove us from this nested
* container, as a replacement command.
*/
removeFromNested()
{
/* to get out of a chair, we simply stand up */
if (gActor.posture == sitting)
replaceAction(Stand);
else
replaceAction(GetOutOf, self);
}
/*
* "sit on" action
*/
dobjFor(SitOn)
{
preCond = (preCondForEntry(sitting))
verify()
{
/* verify entering the chair in a 'sitting' posture */
if (verifyEntry(sitting, &alreadySittingOnMsg, &noRoomToSitMsg))
inherited();
}
action()
{
/* enter the chair in the 'sitting' posture */
performEntry(sitting);
}
}
/*
* "stand on" action
*/
dobjFor(StandOn)
{
/* if it's allowed, use the same preconditions as 'sit on' */
preCond = (preCondForEntry(standing))
verify()
{
/* verify entering in a 'standing' posture */
if (verifyEntry(standing, &alreadyStandingOnMsg,
&noRoomToStandMsg))
inherited();
}
action()
{
/* enter the chair in the 'standing' posture */
performEntry(standing);
}
}
/*
* "lie on" action
*/
dobjFor(LieOn)
{
preCond = (preCondForEntry(lying))
verify()
{
/* verify entering in a 'lying' posture */
if (verifyEntry(lying, &alreadyLyingOnMsg, &noRoomToLieMsg))
inherited();
}
action()
{
/* enter in the 'lying' posture */
performEntry(lying);
}
}
/*
* For "get on/in" / "board", let our default posture object handle
* it, by running the appropriate nested action that moves the actor
* into self in the default posture.
*/
dobjFor(Board)
{
verify() { }
action() { defaultPosture.setActorToPosture(gActor, self); }
}
/* "get off of" is the same as "get out of" */
dobjFor(GetOffOf) asDobjFor(GetOutOf)
/* standard preconditions for sitting/lying/standing on the chair */
preCondForEntry(posture)
{
/*
* if this is not among my allowed postures, we don't need any
* special preconditions, since we'll fail in the verify
*/
if (allowedPostures.indexOf(posture) == nil)
return inherited();
/*
* in order to enter the chair, we have to be able to touch it
* and we have to be able to enter it as a nested room
*/
return [touchObj,
new ObjectPreCondition(self, actorReadyToEnterNestedRoom)];
}
/*
* Verify that we can enter the chair in the given posture. This
* performs verification work common to SIT ON, LIE ON, and STAND ON.
* If this returns true, the caller should inherit the base class
* default handling, otherwise it shouldn't.
*/
verifyEntry(posture, alreadyMsg, noRoomMsg)
{
/*
* if the given posture isn't allowed, tell the caller to use the
* inherited default handling
*/
if (allowedPostures.indexOf(posture) == nil)
return true;
/* this posture is allowed, but it might not be obvious */
if (obviousPostures.indexOf(posture) == nil)
nonObvious;
/*
* If the actor is already on this chair in the given posture,
* this action is redundant. If we already verified okay on this
* point for this same action, ignore the repeated command - it
* must mean that we applied a precondition that did all of our
* work for us (such as moving us out of a nested room
* immediately within us).
*/
if (gActor.posture == posture && gActor.isDirectlyIn(self)
&& gAction.verifiedOkay.indexOf(self) == nil)
illogicalNow(alreadyMsg);
else
gAction.verifiedOkay += self;
/*
* If there's not room for the actor's added bulk, don't allow
* the actor to sit/lie/stand on the chair. If the actor is
* already within the chair, there's no need to add the actor's
* bulk for this change, since it's already counted as being
* within us.
*/
if (!gActor.isIn(self)
&& getBulkWithin() + gActor.getBulk() > bulkCapacity)
illogicalNow(noRoomMsg);
/* we can't sit/stand/lie on something the actor is holding */
if (isIn(gActor))
illogicalNow(&cannotEnterHeldMsg);
/*
* if the actor is already in me, but in a different posture,
* boost the likelihood slightly
*/
if (gActor.isDirectlyIn(self) && gActor.posture != posture)
logicalRank(120, 'already in');
/* tell the caller we don't want to inherit the base class handling */
return nil;
}
/*
* Perform entry in the given posture. This carries out the common
* actions for SIT ON, LIE ON, and STAND ON.
*/
performEntry(posture)
{
/*
* Move the actor into me - this counts as interior travel within
* the enclosing room. Note that we move the actor before
* changing the actor's posture in case the travel fails.
*/
gActor.travelWithin(self);
/* set the actor to the desired posture */
gActor.makePosture(posture);
/* report success */
defaultReport(&roomOkayPostureChangeMsg, posture, self);
}
;
/*
* A Chair is a basic chair with the addition of being a Surface.
*/
class Chair: BasicChair, Surface
/*
* By default, a chair has a seating capacity of one person, so use
* a maximum bulk that only allows one actor to occupy the chair at
* a time.
*/
bulkCapacity = 10
;
/*
* Bed. This is an extension of Chair that allows actors to lie on it
* as well as sit on it. As with chairs, we have a basic bed, plus a
* regular bed that serves as a surface as well.
*/
class BasicBed: BasicChair
/*
* we can sit, lie, and stand on a typical bed, but only sitting and
* lying are obvious default actions
*/
allowedPostures = [sitting, lying, standing]
obviousPostures = [sitting, lying]
/* tryMovingIntoNested failure message is "must sit on chair" */
mustMoveIntoProp = &mustLieOnMsg
/* default posture in this nested room is sitting */
defaultPosture = lying
;
/*
* A Bed is a basic bed with the addition of Surface capabilities.
*/
class Bed: BasicBed, Surface
;
/*
* A Platform is a nested room upon which an actor can stand. In
* general, when you can stand on something, you can also sit and lie on
* it as well (it might not be comfortable, but it is usually at least
* possible), so we make this a subclass of Bed.
*
* The main difference between a platform and a chair that allows
* standing is that a platform is more of a mini-room. In particular,
* items an actor drops while standing on a platform land on the platform
* itself, whereas items dropped while sitting (or standing) on a chair
* land in the enclosing room. In addition, the obvious default action
* for a chair is to sit on it, while the obvious default action for a
* platform is to stand on it.
*/
class BasicPlatform: BasicBed
/*
* we can sit, lie, and stand on a typical platform, and all of
* these could be reasonably expected to be done
*/
allowedPostures = [sitting, lying, standing]
obviousPostures = [sitting, lying, standing]
/* an actor can follow another actor onto or off of a platform */
effectiveFollowLocation = (self)
/* tryMovingIntoNested failure message is "must get on platform" */
mustMoveIntoProp = &mustGetOnMsg
/* default posture in this nested room is sitting */
defaultPosture = standing
/* by default, objects dropped on a platform go onto the platform */
getDropDestination(obj, path)
{
return self;
}
/*
* Remove an actor from the platform. "Get off" is the normal
* command to leave a platform.
*/
tryRemovingFromNested()
{
/* try getting off of the platform */
return tryImplicitAction(GetOffOf, self);
}
/*
* Replace the current action with one that removes the actor from
* this nested room.
*/
removeFromNested()
{
/* get off of the platform */
replaceAction(GetOffOf, self);
}
/*
* Make the actor stand up. On a platform, standing is normally
* allowed, so STAND doesn't usually imply "get off platform" as it
* does in the base class.
*/
makeStandingUp()
{
/*
* If standing isn't among my allowed postures, inherit the
* default behavior, which is to get out of the nested room.
*/
if (allowedPostures.indexOf(standing) == nil)
{
/* we can't stand on the platform, so use the default handling */
inherited();
}
else
{
/* we can stand on the platform, so make the actor stand */
gActor.makePosture(standing);
/* issue a default report of the change */
defaultReport(&roomOkayPostureChangeMsg, standing, self);
}
}
/*
* Traveling 'down' from a platform should generally be taken to
* mean 'get off platform'.
*/
down = noTravelDown
;
/*
* A Platform is a basic platform with the addition of Surface behavior.
*/
class Platform: BasicPlatform, Surface
;
/*
* A "nominal platform" is a named place where NPC's can stand. This
* class makes it easy to arrange for an NPC to be described as standing
* in a particular location in the room: for example, we could have an
* actor "standing in a doorway", or "leaning against the streetlamp."
*
* In most cases, a nominal platform is a "secret" object, in that it
* won't be listed in a room's contents and it won't have any vocabulary
* words. So, the player will never be able to refer to the object in a
* command.
*
* To use this class, instantiate it with an object located in the room
* containing the pseudo-platform. Don't give the object any vocabulary
* words. Locate actors within the pseudo-platform to give them the
* special description.
*
* For simple platform-like "standing on" descriptions, just define the
* name. For descriptions like "standing in", "standing under", or
* "standing near", where only the preposition needs to be customized,
* define the name and define actorInPrep. For more elaborate
* customizations, such as "leaning against the streetlamp", you'll need
* to override roomActorHereDesc, roomActorStatus, roomActorPostureDesc,
* roomListActorPosture, and actorInGroupPrefix/Suffix,
*/
class NominalPlatform: Fixture, Platform
/* don't let anyone stand/sit/lie here via a command */
dobjFor(StandOn) { verify() { illogical(&cannotStandOnMsg); } }
dobjFor(SitOn) { verify() { illogical(&cannotSitOnMsg); } }
dobjFor(LieOn) { verify() { illogical(&cannotLieOnMsg); } }
/* ignore me for 'all' and object defaulting */
hideFromAll(action) { return true; }
hideFromDefault(action) { return true; }
/*
* nominal platforms are internal objects only, not part of the
* visible game world structure, so treat them as equivalent to their
* location for FOLLOW purposes
*/
effectiveFollowLocation = (location.effectiveFollowLocation)
;
/*
* A booth is a nested room that serves as a small enclosure within a
* larger room. Booths can serve as regular containers as well as
* nested rooms, and can be made openable by addition of the Openable
* mix-in class. Note that booths don't have to be fully enclosed, nor
* do they actually have to be closable.
*
* Examples of booths: a cardboard box large enough for an actor can
* stand in; a closet; a shallow pit.
*/
class Booth: BasicPlatform, Container
/*
* Try an implied command to move the actor from outside of this
* nested room into this nested room.
*/
tryMovingIntoNested()
{
/* try getting in me */
return tryImplicitAction(Board, self);
}
/*
* Remove an actor from the booth. "Get out" is the normal command
* to leave this type of room.
*/
tryRemovingFromNested()
{
/* try getting out of the object */
return tryImplicitAction(GetOutOf, self);
}
/*
* Replace the current action with one that removes the actor from
* this nested room.
*/
removeFromNested()
{
/* get out of the object */
replaceAction(GetOutOf, self);
}
/*
* "Enter" is equivalent to "get in" (or "board") for a booth
*/
dobjFor(Enter) asDobjFor(Board)
/* explicitly define the push-travel indirect object mapping */
mapPushTravelIobj(PushTravelEnter, Board)
;
/* ------------------------------------------------------------------------ */
/*
* A Vehicle is a special type of nested room that moves instead of the
* actor in response to travel commands. When an actor in a vehicle
* types, for example, "go north," the vehicle moves north, not the
* actor.
*
* In most cases, a Vehicle should multiply inherit from one of the
* other nested room subclasses to make it more specialized. For
* example, a bicycle might inherit from Chair, so that actors can sit
* on the bike.
*
* Note that because Vehicle inherits from NestedRoom, the OUT direction
* in the vehicle by default means what it does in NestedRoom -
* specifically, getting out of the vehicle. This is appropriate for
* vehicles where we'd describe passengers as being inside the vehicle,
* such as a car or a boat. However, if the vehicle is something you
* ride on, like a horse or a bike, it's probably more appropriate for
* OUT to mean "ride the vehicle out of the enclosing room." To get
* this effect, simply override the "out" property and set it to nil;
* this will prevent the NestedRoom definition from being inherited,
* which will make us look for the OUT location of the enclosing room as
* the travel destination.
*/
class Vehicle: NestedRoom, Traveler
/*
* When a traveler is in a vehicle, and the traveler performs a
* travel command, the vehicle is what changes location; the
* contained traveler simply stays put while the vehicle moves.
*/
getLocTraveler(trav, conn)
{
local stage;
/*
* If the connector is contained within the vehicle, take it as
* leading out of the vehicle - this means we want to move
* traveler within the vehicle rather than the vehicle.
* Consider the connector be inside the vehicle if it's a
* physical object (a Thing) that's inside the vehicle, or one
* of the vehicle's own directional links points directly to the
* connector.
*
* Likewise, if the connector is marked with the vehicle or any
* object inside the vehicle as its staging location, the
* vehicle obviously isn't involved in the travel.
*/
if ((conn != nil && conn.ofKind(Thing) && conn.isIn(self))
|| Direction.allDirections.indexWhich(
{dir: self.(dir.dirProp) == conn}) != nil
|| ((stage = conn.connectorStagingLocation) != nil
&& (stage == self || stage.isIn(self))))
{
/*
* this connector leads out from within the vehicle - move
* the inner traveler rather than the vehicle itself
*/
return trav;
}
/*
* If we have a location, ask it who travels when the VEHICLE is
* the traveler within it; otherwise, the vehicle is the
* traveler.
*
* We ask the location, because the location might itself be a
* vehicle, in which case it might want us to be driving around
* the enclosing vehicle. However, we pass ourselves (i.e.,
* this vehicle) as the inner traveler, rather than the traveler
* we were passed, because a traveler within a vehicle moves the
* vehicle when traveling.
*/
return (location != nil ? location.getLocTraveler(self, conn) : self);
}
/*
* An OUT command while within a vehicle could mean one of two
* things: either to GET OUT of the vehicle, or to ride/drive the
* vehicle out of its enclosing location.
*
* There's no good way of guessing which meaning the player intends,
* so we have to choose one or the other. We choose the ride/drive
* interpretation as the default, for two reasons. First, it seems
* to be what most players expect. Second, the other interpretation
* leaves no standard way of expressing the ride/drive meaning. We
* return nil here to indicate to the OUT action that we want the
* enclosing location's 'out' connector to be used while an actor is
* in the vehicle.
*
* For some vehicles, it might be more appropriate for OUT to mean
* GET OUT. In these cases, simply override this so that it returns
* nestedRoomOut.
*/
out = nil
/*
* Get the "location push traveler" - this is the traveler when a
* push-travel command is performed by a traveler within this
* location. If the object we're trying to push is within me, use
* the contained traveler, since the contained traveler must be
* trying to push the object around directly. If the object isn't
* inside me, then we're presumably trying to use the vehicle to push
* around the object, so the traveler is the vehicle or something
* containing the vehicle.
*/
getLocPushTraveler(trav, obj)
{
/*
* If the object is inside me, use the nested traveler;
* otherwise, we're presumably trying to use the vehicle to move
* the object.
*/
if (obj.isIn(self))
{
/*
* we're moving something around inside me; use the
* contained traveler
*/
return trav;
}
else if (location != nil)
{
/*
* we're pushing something around outside me, so we're
* probably trying to use the vehicle to do so; we have a
* location, so ask it what it thinks, passing myself as the
* new suggested traveler
*/
return location.getLocPushTraveler(self, obj);
}
else
{
/*
* we're pushing something around outside me, and I have no
* location, so I must be the traveler
*/
return self;
}
}
/*
* Determine if an actor is traveling with me. The normal base
* class implementation works, but it's more efficient just to check
* to see if the actor is inside this object than to construct the
* entire nested contents list just to check to see if the actor's
* in that list.
*/
isActorTraveling(actor) { return actor.isIn(self); }
/* invoke a callback for each actor traveling with us */
forEachTravelingActor(func)
{
/* invoke the callback on each actor in our contents */
allContents().forEach(function(obj) {
if (obj.isActor)
(func)(obj);
});
}
/*
* Get the actors involved in the travel. This is a list consisting
* of all of the actors contained within the vehicle.
*/
getTravelerActors = (allContents().subset({x: x.isActor}))
/*
* there are no self-motive actors in a vehicle - the vehicle is
* doing the travel, and the actors within are just moving along
* with it as cargo
*/
getTravelerMotiveActors = []
/*
* Traveler preconditions for the vehicle. By default, we add no
* preconditions of our own, but specific vehicles might want to
* override this. For example, a car might want to require that the
* doors are closed, the engine is running, and the seatbelts are
* fastened before it can travel.
*/
travelerPreCond(conn) { return []; }
/*
* Check, using pre-condition rules, that the traveler is in the
* given room, moving the traveler to the room if possible.
*/
checkMovingTravelerInto(room, allowImplicit)
{
/* if we're in the desired location, we're set */
if (isDirectlyIn(room))
return nil;
/*
* By default, we can't move a vehicle into a room implicitly.
* Individual vehicles can override this when there's an obvious
* way of moving the vehicle in and out of nested rooms.
*/
reportFailure(&vehicleCannotDoFromMsg, self);
exit;
}
/*
* the lister object we use to display the list of actors aboard, in
* arrival and departure messages for the vehicle
*/
aboardVehicleListerObj = aboardVehicleLister
;
/*
* A VehicleBarrier is a TravelConnector that allows actors to travel,
* but blocks vehicles. By default, we block all vehicles, but
* subclasses can customize this so that we block only specific
* vehicles.
*/
class VehicleBarrier: TravelBarrier
/*
* Determine if the given traveler can pass through this connector.
* By default, we'll return nil for a Vehicle, true for anything
* else. This can be overridden to allow specific vehicles to pass,
* or to filter on any other criteria.
*/
canTravelerPass(traveler) { return !traveler.ofKind(Vehicle); }
/* explain why we can't pass */
explainTravelBarrier(traveler)
{
reportFailure(&cannotGoThatWayInVehicleMsg, traveler);
}
;
TADS 3 Library Manual
Generated on 5/16/2013 from TADS version 3.1.3