extras.t
#charset "us-ascii"
/*
* Copyright (c) 2000, 2006 Michael J. Roberts. All Rights Reserved.
*
* TADS 3 Library - extras: special-purpose object classes
*
* This module defines classes for specialized simulation objects.
*
* Portions are based on original work by Eric Eve, incorporated by
* permission.
*/
/* include the library header */
#include "adv3.h"
/* ------------------------------------------------------------------------ */
/*
* A "complex" container is an object that can have multiple kinds of
* contents simultaneously. For example, a complex container could act
* as both a surface, so that some objects are sitting on top of it, and
* simultaneously as a container, with objects inside.
*
* The standard containment model only allows one kind of containment per
* container, because the nature of the containment is a feature of the
* container itself. The complex container handles multiple simultaneous
* containment types by using one or more sub-containers: for example, if
* we want to be able to act as both a surface and a regular container,
* we use two sub-containers, one of class Surface and one of class
* Container, to hold the different types of contents. When we need to
* perform an operation specific to a certain containment type, we
* delegate the operation to the sub-container of the appropriate type.
*
* Note that the complex container itself treats its direct contents as
* components, so any component parts can be made direct contents of the
* complex container object.
*
* If you want to include objects in your source code that are initially
* located within the component sub-containers, define them as directly
* within the ComplexContainer object, but give each one a 'subLocation'
* property set to the property of the component sub-container that will
* initially contain it. For example, here's how you'd place a blanket
* inside a washing machine, and a laundry basket on top of it:
*
*. + washingMachine: ComplexContainer 'washing machine' 'washing machine'
*. subContainer: ComplexComponent, Container { etc }
*. subSurface: ComplexComponent, Surface { etc }
*. ;
*.
*. ++ Thing 'big cotton blanket' 'blanket'
*. subLocation = &subContainer
*. ;
*.
*. ++ Container 'laundry basket' 'laundry basket'
*. subLocation = &subSurface
*. ;
*
* The subLocation setting is only used for initialization, and we
* automatically set it to nil right after we use it to set up the
* initial location. If you want to move something into one of the
* sub-containers on the fly, simply refer to the desired component
* directly:
*
* pants.moveInto(washingMachine.subContainer);
*/
class ComplexContainer: Thing
/*
* Our inner container, if any. This is a "secret" object (in other
* words, it doesn't appear to players as a separate named object)
* that we use to store the contents that are meant to be within the
* complex container. If this is to be used, it should be set to a
* Container object - the most convenient way to do this is by using
* the nested object syntax to define a ComplexComponent Container
* instance, like so:
*
* washingMachine: ComplexContainer
*. subContainer: ComplexComponent, Container { etc }
*. ;
*
* Note that we use the ComplexComponent class (as well as
* Container) for the sub-container object. This makes the
* sub-container automatically use the name of its enclosing object
* in messages (in this case, the sub-container will use the same
* name as the washing machine).
*
* Note that the sub-containers don't have to be of class
* ComplexComponent, but using that class makes your job a little
* easier because the class sets the location and naming
* automatically. If you prefer to define your sub-containers as
* separate objects, not nested in the ComplexContainer's
* definition, there's no need to make them ComplexComponents; just
* make them ordinary Component objects.
*
* If this property is left as nil, then we don't have an inner
* container.
*/
subContainer = nil
/*
* Our inner surface, if any. This is a secret object like the
* inner container; this object acts as our surface.
*/
subSurface = nil
/*
* Our underside, if any. This is a secret object like the inner
* container; this object can act as the space underneath us, or as
* our bottom surface.
*/
subUnderside = nil
/*
* Our rear surface or container, if any. This is a secret internal
* object like the inner container; this object can act as our back
* surface, or as the space just behind us.
*/
subRear = nil
/* a list of all of our component objects */
allSubLocations = [subContainer, subSurface, subUnderside, subRear]
/*
* Show our status. We'll show the status for each of our
* sub-objects, so that we list any contents of our sub-container or
* sub-surface along with our description.
*/
examineStatus()
{
/* if we have a sub-container, show its status */
if (subContainer != nil)
subContainer.examineStatus();
/* if we have a sub-surface, show its status */
if (subSurface != nil)
subSurface.examineStatus();
/* if we have a sub-rear, show its status */
if (subRear != nil)
subRear.examineStatus();
/* if we have a sub-underside, show its status */
if (subUnderside != nil)
subUnderside.examineStatus();
}
/*
* In most cases, the open/closed and locked/unlocked status of a
* complex container refer to the status of the sub-container.
*/
isOpen = (subContainer != nil ? subContainer.isOpen : inherited)
isLocked = (subContainer != nil ? subContainer.isLocked : inherited)
makeOpen(stat)
{
if (subContainer != nil)
subContainer.makeOpen(stat);
else
inherited(stat);
}
makeLocked(stat)
{
if (subContainer != nil)
subContainer.makeLocked(stat);
else
inherited(stat);
}
/*
* route all commands that treat us as a container to our
* sub-container object
*/
dobjFor(Open) maybeRemapTo(subContainer != nil, Open, subContainer)
dobjFor(Close) maybeRemapTo(subContainer != nil, Close, subContainer)
dobjFor(LookIn) maybeRemapTo(subContainer != nil, LookIn, subContainer)
iobjFor(PutIn) maybeRemapTo(subContainer != nil,
PutIn, DirectObject, subContainer)
dobjFor(Lock) maybeRemapTo(subContainer != nil, Lock, subContainer)
dobjFor(LockWith) maybeRemapTo(subContainer != nil,
LockWith, subContainer, IndirectObject)
dobjFor(Unlock) maybeRemapTo(subContainer != nil, Unlock, subContainer)
dobjFor(UnlockWith) maybeRemapTo(subContainer != nil,
UnlockWith, subContainer, IndirectObject)
/* route commands that treat us as a surface to our sub-surface */
iobjFor(PutOn) maybeRemapTo(subSurface != nil,
PutOn, DirectObject, subSurface)
/* route commands that affect our underside to our sub-underside */
iobjFor(PutUnder) maybeRemapTo(subUnderside != nil,
PutUnder, DirectObject, subUnderside)
dobjFor(LookUnder) maybeRemapTo(subUnderside != nil,
LookUnder, subUnderside)
/* route commands that affect our rear to our sub-rear-side */
iobjFor(PutBehind) maybeRemapTo(subRear != nil,
PutBehind, DirectObject, subRear)
dobjFor(LookBehind) maybeRemapTo(subRear != nil, LookBehind, subRear)
/* route commands relevant to nested rooms to our components */
dobjFor(StandOn) maybeRemapTo(getNestedRoomDest(StandOnAction) != nil,
StandOn, getNestedRoomDest(StandOnAction))
dobjFor(SitOn) maybeRemapTo(getNestedRoomDest(SitOnAction) != nil,
SitOn, getNestedRoomDest(SitOnAction))
dobjFor(LieOn) maybeRemapTo(getNestedRoomDest(LieOnAction) != nil,
LieOn, getNestedRoomDest(LieOnAction))
dobjFor(Board) maybeRemapTo(getNestedRoomDest(BoardAction) != nil,
Board, getNestedRoomDest(BoardAction))
dobjFor(Enter) maybeRemapTo(getNestedRoomDest(EnterAction) != nil,
Enter, getNestedRoomDest(EnterAction))
/* map GET OUT/OFF to whichever complex component we're currently in */
dobjFor(GetOutOf) maybeRemapTo(getNestedRoomSource(gActor) != nil,
GetOutOf, getNestedRoomSource(gActor))
dobjFor(GetOffOf) maybeRemapTo(getNestedRoomSource(gActor) != nil,
GetOffOf, getNestedRoomSource(gActor))
/*
* Get the destination for nested travel into this object. By
* default, we'll look at the sub-container and sub-surface
* components to see if either is a nested room, and if so, we'll
* return that. The surface takes priority if both are nested rooms.
*
* You can override this to differentiate by verb, if desired; for
* example, you could have SIT ON and LIE ON refer to the sub-surface
* component, while ENTER and BOARD refer to the sub-container
* component.
*
* Note that if you do need to override this method to distinguish
* between a sub-container ("IN") and a sub-surface ("ON") for nested
* room purposes, there's a subtlety to watch out for. The English
* library maps "sit on" and "sit in" to the single Action SitOn;
* likewise with "lie in/on" for LieOn and "stand in/on" for StandOn.
* If you're distinguishing the sub-container from the sub-surface,
* you'll probably want to distinguish SIT IN from SIT ON (and
* likewise for LIE and STAND). Fortunately, even though the action
* class is the same for both phrasings, you can still find out
* exactly which preposition the player typed using
* action.getEnteredVerbPhrase().
*/
getNestedRoomDest(action)
{
/*
* check the sub-surface first to see if it's a nested room;
* failing that, check the sub-container; failing that, we don't
* have a suitable component destination
*/
if (subSurface != nil && subSurface.ofKind(NestedRoom))
return subSurface;
else if (subContainer != nil && subContainer.ofKind(NestedRoom))
return subContainer;
else
return nil;
}
/*
* Get the source for nested travel out of this object. This is used
* for GET OUT OF <self> - we figure out which nested room component
* the actor is in, so that we can remap the command to GET OUT OF
* <that component>.
*/
getNestedRoomSource(actor)
{
/* figure out which child the actor is in */
foreach (local chi in allSubLocations)
{
if (chi != nil && actor.isIn(chi))
return chi;
}
/* the actor doesn't appear to be in one of our component locations */
return nil;
}
/*
* Get a list of objects suitable for matching ALL in TAKE ALL FROM
* <self>. By default, if we have a sub-surface and/or
* sub-container, we return everything in scope that's inside either
* one of those. Otherwise, if we have a sub-rear-surface and/or an
* underside, we'll return everything from those.
*/
getAllForTakeFrom(scopeList)
{
local containers;
/*
* Make a list of the containers in which we're going to look.
* If we have a sub-container or sub-surface, look only in those.
* Otherwise, if we have a rear surface or underside, look in
* those.
*/
containers = [];
if (subContainer != nil)
containers += subContainer;
if (subSurface != nil)
containers += subSurface;
if (containers == [])
{
if (subRear != nil)
containers += subRear;
if (subUnderside != nil)
containers += subUnderside;
}
/*
* return the list of everything in scope that's directly in one
* of the selected containers, but isn't a component of its
* direct container
*/
return scopeList.subset(
{x: (x != self
&& containers.indexOf(x) == nil
&& containers.indexWhich(
{c: x.isDirectlyIn(c) && !x.isComponentOf(c)}) != nil)});
}
/*
* Add an object to my contents. If the object has a subLocation
* setting, take it as indicating which of my subcontainers is to
* contain the object.
*/
addToContents(obj)
{
local sub;
/*
* if the object has a subLocation, add it to my appropriate
* component object; if not, add to my own contents as usual
*/
if ((sub = obj.subLocation) != nil)
{
/*
* It specifies a subLocation - add it to the corresponding
* component's contents. Note that subLocation is a property
* pointer - &subContainer for my container component,
* &subSurface for my surface component, etc.
*/
self.(sub).addToContents(obj);
/*
* The object's present location is merely for set-up
* purposes, so that the '+' object definition notation can
* be used to give the object its initial location. The
* object really wants to be in the sub-container, to whose
* contents list we've just added it. Set its location to
* the sub-container.
*/
obj.location = self.(sub);
/*
* Now that we've moved the object into its sub-location,
* forget the subLocation setting, since this property is
* only for initialization.
*/
obj.subLocation = nil;
}
else
{
/* there's no subLocation, so use the default handling */
inherited(obj);
}
}
/*
* If we have any SpaceOverlay children, abandon the contents of the
* overlaid spaces as needed.
*/
mainMoveInto(newCont)
{
/*
* If any of our components are SpaceOverlays, notify them. We
* only worry about the rear and underside components, since it's
* never appropriate for our container and surface components to
* act as space overlays.
*/
notifyComponentOfMove(subRear);
notifyComponentOfMove(subUnderside);
/* do the normal work */
inherited(newCont);
}
/*
* if we're being pushed into a new location (as a PushTraveler),
* abandon the contents of any SpaceOverlay components
*/
beforeMovePushable(traveler, connector, dest)
{
/*
* notify our SpaceOverlay components that we're being moved, if
* we're going to end up in a new location
*/
if (dest != location)
{
/* notify our rear and underside components of the move */
notifyComponentOfMove(subRear);
notifyComponentOfMove(subUnderside);
}
/* do the normal work */
inherited(traveler, connector, dest);
}
/*
* if the given component is a SpaceOverlay, notify it that we're
* moving, so that it can abandon its contents as needed
*/
notifyComponentOfMove(sub)
{
/* if it's a SpaceOverlay, abandon its contents if necessary */
if (sub != nil && sub.ofKind(SpaceOverlay))
sub.abandonContents();
}
/* pass bag-of-holding operations to our sub-container */
tryPuttingObjInBag(target)
{
/* if we have a subcontainer, let it handle the operation */
return (subContainer != nil
? subContainer.tryPuttingObjInBag(target)
: nil);
}
/* pass implicit PUT x IN self operations to our subcontainer */
tryMovingObjInto(obj)
{
/* if we have a subcontainer, let it handle the operation */
return (subContainer != nil
? subContainer.tryMovingObjInto(obj)
: nil);
}
;
/*
* we don't actually define any subLocation property values anywhere, so
* declare it to make sure the compiler knows it's a property name
*/
property subLocation;
/*
* A component object of a complex container. This class can be used as
* a mix-in for sub-objects of a complex container (the subContainer or
* subSurface) defined as nested objects.
*
* This class is based on Component, which is suitable for complex
* container sub-objects because it makes them inseparable from the
* complex container. It's also based on NameAsParent, which makes the
* object automatically use the same name (in messages) as the lexical
* parent object. This is usually what one wants for a sub-object of a
* complex container, because it makes the sub-object essentially
* invisible to the user by referring to the sub-object in messages as
* though it were the complex container itself: "The washing machine
* contains...".
*
* This class also automatically initializes our location to our lexical
* parent, during the pre-initialization process. Any of these that are
* dynamically created at run-time (using 'new') must have their
* locations set manually, because initializeLocation() won't be called
* automatically in those cases.
*/
class ComplexComponent: Component, NameAsParent
initializeLocation()
{
/* set our location to our lexical parent */
location = lexicalParent;
/* inherit default so we initialize our container's 'contents' list */
inherited();
}
/*
* Get our "identity" object. We take our identity from our parent
* object, if we have one. Note that our identity isn't simply our
* parent, but rather is our parent's identity, recursively defined.
*/
getIdentityObject()
{
return (location != nil ? location.getIdentityObject() : self);
}
/* don't participate in 'all', since we're a secret internal object */
hideFromAll(action) { return true; }
/*
* In case this component is being used to implement a nested room of
* some kind (a platform, booth, etc), use the complex container's
* location as the staging location. Normally our staging location
* would be our direct container, but as with other aspects of
* complex containers, the container/component are meant to act as a
* single combined object, so we'd want to bypass the complex
* container and move directly between the enclosing location and
* 'self'.
*/
stagingLocations = [lexicalParent.location]
;
/*
* A container door. This is useful for cases where you want to create
* the door to a container as a separate object in its own right.
*/
class ContainerDoor: Component
/*
* In most cases, you should create a ContainerDoor as a component of
* a ComplexContainer. It's usually necessary to use a
* ComplexContainer in order to use a door, since the door has to go
* somewhere, and it can't go inside the container it controls
* (because if it were inside, it wouldn't be accessible when the
* container is closed).
*
* By default, we assume that our immediate location is a complex
* container, and its subContainer is the actual container for which
* we're the door. You can override this property to create a
* different relationship if necessary.
*/
subContainer = (location.subContainer)
/* we're open if our associated sub-container is open */
isOpen = (subContainer.isOpen)
/* our status description mentions our open status */
examineStatus()
{
/* add our open status */
say(isOpen
? gLibMessages.currentlyOpen : gLibMessages.currentlyClosed);
/* add the base class behavior */
inherited();
}
/* looking in or behind a door is like looking inside the container */
dobjFor(LookIn) remapTo(LookIn, subContainer)
dobjFor(LookBehind) remapTo(LookIn, subContainer)
/* door-like operations on the door map to the container */
dobjFor(Open) remapTo(Open, subContainer)
dobjFor(Close) remapTo(Close, subContainer)
dobjFor(Lock) remapTo(Lock, subContainer)
dobjFor(LockWith) remapTo(LockWith, subContainer, IndirectObject)
dobjFor(Unlock) remapTo(Unlock, subContainer)
dobjFor(UnlockWith) remapTo(UnlockWith, subContainer, IndirectObject)
;
/* ------------------------------------------------------------------------ */
/*
* A "space overlay" is a special type of container whose contents are
* supposed to be adjacent to the container object (i.e., self), but are
* not truly contained in the usual sense. This is used to model spatial
* relationships such as UNDER and BEHIND, which aren't directly
* supported in the normal containment model.
*
* The special feature of a space overlay is that the contents aren't
* truly attached to the container object, so they don't move with it the
* way that the contents of an ordinary container do. For example,
* suppose we have a space overlay representing a bookcase and the space
* behind it, so that we can hide a painting behind the bookcase: in this
* case, moving the bookcase should leave the painting where it was,
* because it was just sitting there in that space. In the real world,
* of course, the painting was sitting on the floor all along, so moving
* the bookcase would have no effect on it; but our spatial relationship
* model isn't quite as good as reality's, so we have to resort to an
* extra fix-up step. Specifically, when we move a space overlay, we
* always check to see if its contents need to be relocated to the place
* where they were really supposed to be all along.
*/
class SpaceOverlay: BulkLimiter
/*
* If we move this object, the objects we contain might stay put
* rather than moving along with the container. For example, if we
* represent the space behind a bookcase, moving the bookcase would
* leave objects that were formerly behind the bookcase just sitting
* on the floor (or attached to the wall, or whatever).
*/
mainMoveInto(newContainer)
{
/* check to see if our objects need to be left behind */
abandonContents();
/* now do the normal work */
inherited(newContainer);
}
/*
* when we're being pushed to a new location via push-travel, abandon
* our contents before we're moved
*/
beforeMovePushable(traveler, connector, dest)
{
/* check to see if our objects need to be left behind */
if (dest != getIdentityObject().location)
abandonContents();
/* do the normal work */
inherited(traveler, connector, dest);
}
/*
* abandonLocation is where the things under me end up when I'm
* moved.
*
* An Underside or RearContainer represents an object that has a
* space underneath or behind it, respectively, but the space itself
* isn't truly part of the container object (i.e., self). This
* means that when the container moves, the objects under/behind it
* shouldn't move. For example, if there's a box under a bed,
* moving the bed out of the room should leave the box sitting on
* the floor where the bed used to be.
*
* By default, our abandonLocation is simply the location of our
* "identity object" - that is, the location of our nearest
* enclosing object that isn't a component.
*
* This can be overridden if the actual abandonment location should
* be somewhere other than our assembly location. In addition, you
* can set this to nil to indicate that objects under/behind me will
* NOT be abandoned when I move; instead, they'll simply stay with
* me, as though they're attached to my underside/back surface.
*/
abandonLocation = (getIdentityObject().location)
/*
* By default we list our direct contents the first time we're
* moved, and ONLY the first time. If alwaysListOnMove is
* overridden to true, then we'll list our contents EVERY time we're
* moved. If neverListOnMove is set to true, then we'll NEVER list
* our contents automatically when moved; this can be used in cases
* where the game wants to produce its own listing explicitly,
* rather than using the default listing we generate. (Obviously,
* setting both 'always' and 'never' is meaningless, but in case
* you're wondering, 'never' overrides 'always' in this case.)
*
* Setting abandonLocation to nil overrules alwaysListOnMove: if
* there's no abandonment, then we consider nothing to be revealed
* when we're moved, since my contents move along with me.
*/
alwaysListOnMove = nil
neverListOnMove = nil
/*
* The lister we use to describe the objects being revealed when we
* move the SpaceOverlay object and abandon the contents. Each
* concrete kind of SpaceOverlay must provide a lister that uses
* appropriate language; the list should be roughly of the form
* "Moving the armoire reveals a rusty can underneath." Individual
* objects can override this to customize the message further.
*/
abandonContentsLister = nil
/*
* Abandon my contents when I'm moved. This is called whenever we're
* moved to a new location, to take care of leaving behind the
* objects that were formerly under me.
*
* We'll move my direct contents into abandonLocation, unless that's
* set to nil. We don't move any Component objects within me, since
* we assume those to be attached.
*/
abandonContents()
{
local dest;
/*
* if there's no abandonment location, our contents move with us,
* so there's nothing to do
*/
if ((dest = abandonLocation) == nil)
return;
/*
* If we've never been moved before, or we always reveal my
* contents when moved, list our contents now. In any case, if
* we *never* list on move, don't generate the listing.
*/
if ((alwaysListOnMove || !getIdentityObject().moved)
&& !neverListOnMove)
{
local marker1, marker2;
/*
* We want to generate a listing of what is revealed by
* moving the object, which we can do by generating a
* listing of what we would normally see by looking in the
* overlay interior. We want the listing as it stands now,
* but in most cases, we don't actually want to generate the
* list quite yet, because we want the action that's moving
* the object to complete and show all of its messages first.
*
* However, if the action leaves the actor in a new
* location, do generate the listing before the rest of the
* action output, since the listing won't make any sense
* after we've moved to (and displayed the description of)
* the new location.
*
* To accomplish all of this, generate the listing now,
* before the rest of the action output, but insert special
* report markers into the transcript before and after the
* listing. Then, register to receive after-action
* notification; at the end of the action, we'll go back and
* move the range of transcript output between the markers
* to the end of the command's transcript entries, if we
* haven't moved to a new room.
*
* One final complication: we don't want our listing here to
* hide any default report from the main command, so run it
* as a sub-action. A sub-action doesn't override the
* visibility of its parent's default report.
*/
/* first, add a marker to the transcript before the listing */
marker1 = gTranscript.addMarker();
/* generate the listing, using a generic sub-action context */
withActionEnv(Action, gActor, {: listContentsForMove() });
/* add another transcript marker after the listing */
marker2 = gTranscript.addMarker();
/*
* create our special handler object to receive notification
* at the end of the command - it'll move the reports to the
* end of the command output if need be
*/
new SpaceOverlayAbandonFinisher(marker1, marker2);
}
/* now move my non-Component contents to the abandonment location */
foreach(obj in contents)
{
/* if it's not a component, move it */
if(!obj.ofKind(Component))
obj.moveInto(dest);
}
}
/*
* My weight does NOT include my "contents" if we abandon our
* contents on being moved. Our contents are not attached to us as
* they are in a normal sort of container; instead, they're merely
* colocated, so when we're moved, that colocation relationship ends.
*/
getWeight()
{
/*
* if we abandon our contents on being moved, our weight doesn't
* include them, because they're not attached; otherwise, they do
* act like they're attached, and hence must be included in our
* weight as for any ordinary container
*/
if (abandonLocation != nil)
{
/*
* our contents are not included in our weight, so our total
* weight is simply our own intrinsic weight
*/
return weight;
}
else
{
/* our contents are attached, so include their weight as normal */
return inherited();
}
}
/*
* List our contents for moving the object. By default, we examine
* our interior using our abandonContentsLister.
*/
listContentsForMove()
{
/* examine our contents with the abandonContentsLister */
examineInteriorWithLister(abandonContentsLister);
}
;
/*
* Space Overlay Abandon Finisher - this is an internal object that we
* create in SpaceOverlay.abandonContents(). Its purpose is to receive
* an afterAction notification and clean up the report order if
* necessary.
*/
class SpaceOverlayAbandonFinisher: object
construct(m1, m2)
{
/* remember the markers */
marker1 = m1;
marker2 = m2;
/* remember the actor's starting location */
origLocation = gActor.location;
/* register for afterAction notification */
gAction.addBeforeAfterObj(self);
}
/* the transcript markers identifying the listing reports */
marker1 = nil
marker2 = nil
/* the actor's location at the time we generated the listing */
origLocation = nil
/* receive our after-action notification */
afterAction()
{
/*
* If the actor hasn't changed locations, move the reports we
* generated for the listing to the end of the transcript.
*/
if (gActor.location == origLocation)
gTranscript.moveRangeAppend(marker1, marker2);
}
;
/*
* An "underside" is a special type of container that describes its
* contents as being under the object. This is appropriate for objects
* that have a space underneath, such as a bed or a table.
*/
class Underside: SpaceOverlay
/*
* Can actors put new objects under self, using the PUT UNDER
* command? By default, we allow it. Override this property to nil
* if new objects cannot be added by player commands.
*/
allowPutUnder = true
/* we need to LOOK UNDER this object to see its contents */
nestedLookIn() { nestedAction(LookUnder, self); }
/* use custom contents listers, for our special "under" wording */
contentsLister = undersideContentsLister
descContentsLister = undersideDescContentsLister
lookInLister = undersideLookUnderLister
inlineContentsLister = undersideInlineContentsLister
abandonContentsLister = undersideAbandonContentsLister
/* customize the message for taking something from that's not under me */
takeFromNotInMessage = &takeFromNotUnderMsg
/* customize the message indicating another object is already in me */
circularlyInMessage = &circularlyUnderMsg
/* message phrase for objects put under me */
putDestMessage = &putDestUnder
/* message when we don't have room to put another object under me */
tooFullMsg = &undersideTooFull
/* message when an object is too large (all by itself) to fit under me */
tooLargeForContainerMsg = &tooLargeForUndersideMsg
/* can't put self under self */
cannotPutInSelfMsg = &cannotPutUnderSelfMsg
/* can't put something under me when it's already under me */
alreadyPutInMsg = &alreadyPutUnderMsg
/* our implied containment verb is PUT UNDER */
tryMovingObjInto(obj) { return tryImplicitAction(PutUnder, obj, self); }
/* -------------------------------------------------------------------- */
/*
* Handle putting things under me
*/
iobjFor(PutUnder)
{
verify()
{
/* use the standard put-in-interior verification */
verifyPutInInterior();
}
check()
{
/* only allow it if PUT UNDER commands are allowed */
if (!allowPutUnder)
{
reportFailure(&cannotPutUnderMsg);
exit;
}
}
action()
{
/* move the direct object onto me */
gDobj.moveInto(self);
/* issue our default acknowledgment */
defaultReport(&okayPutUnderMsg);
}
}
/*
* Looking "under" a surface simply shows the surface's contents.
*/
dobjFor(LookUnder)
{
verify() { }
action() { examineInterior(); }
}
;
/*
* A special kind of Underside that only accepts specific contents.
*/
class RestrictedUnderside: RestrictedHolder, Underside
/*
* A message that explains why the direct object can't be put under
* this object. In most cases, the rather generic default message
* should be overridden to provide a specific reason that the dobj
* can't be put under me. The rejected object is provided as a
* parameter in case the message needs to vary by object, but we
* ignore this and just use a single blanket failure message by
* default.
*/
cannotPutUnderMsg(obj) { return &cannotPutUnderRestrictedMsg; }
/* override PutUnder to enforce our contents restriction */
iobjFor(PutUnder) { check() { checkPutDobj(&cannotPutUnderMsg); } }
;
/*
* A "rear container" is similar to an underside: it models the space
* behind an object.
*/
class RearContainer: SpaceOverlay
/*
* Can actors put new objects behind self, using the PUT BEHIND
* command? By default, we allow it. Override this property to nil
* if new objects cannot be added by player commands.
*/
allowPutBehind = true
/* we need to LOOK BEHIND this object to see its contents */
nestedLookIn() { nestedAction(LookBehind, self); }
/* use custom contents listers */
contentsLister = rearContentsLister
descContentsLister = rearDescContentsLister
lookInLister = rearLookBehindLister
inlineContentsLister = rearInlineContentsLister
abandonContentsLister = rearAbandonContentsLister
/* the message for taking things from me that aren't behind me */
takeFromNotInMessage = &takeFromNotBehindMsg
/*
* my message indicating that another object x cannot be put into me
* because I'm already in x
*/
circularlyInMessage = &circularlyBehindMsg
/* message phrase for objects put under me */
putDestMessage = &putDestBehind
/* message when we're too full for another object */
tooFullMsg = &rearTooFullMsg
/* message when object is too large to fit behind me */
tooLargeForContainerMsg = &tooLargeForRearMsg
/* customize the verification messages */
cannotPutInSelfMsg = &cannotPutBehindSelfMsg
alreadyPutInMsg = &alreadyPutBehindMsg
/* our implied containment verb is PUT BEHIND */
tryMovingObjInto(obj) { return tryImplicitAction(PutBehind, obj, self); }
/* -------------------------------------------------------------------- */
/*
* Handle the PUT UNDER command
*/
iobjFor(PutBehind)
{
verify() { verifyPutInInterior(); }
check()
{
/* only allow it if PUT BEHIND commands are allowed */
if (!allowPutBehind)
{
reportFailure(&cannotPutBehindMsg);
exit;
}
}
action()
{
/* move the direct object behind me */
gDobj.moveInto(self);
/* issue our default acknowledgment */
defaultReport(&okayPutBehindMsg);
}
}
/*
* Looking "behind" a surface simply shows the surface's contents.
*/
dobjFor(LookBehind)
{
verify() { }
action() { examineInterior(); }
}
;
/*
* A special kind of RearContainer that only accepts specific contents.
*/
class RestrictedRearContainer: RestrictedHolder, RearContainer
/*
* A message that explains why the direct object can't be put behind
* this object. In most cases, the rather generic default message
* should be overridden to provide a specific reason that the dobj
* can't be put behind me. The rejected object is provided as a
* parameter in case the message needs to vary by object, but we
* ignore this and just use a single blanket failure message by
* default.
*/
cannotPutBehindMsg(obj) { return &cannotPutBehindRestrictedMsg; }
/* override PutBehind to enforce our contents restriction */
iobjFor(PutBehind) { check() { checkPutDobj(&cannotPutBehindMsg); } }
;
/*
* A "rear surface" is essentially the same as a "rear container," but
* models the contents as being attached to the back of the object rather
* than merely sitting behind it.
*
* The only practical difference between the "container" and the
* "surface" is that moving a surface moves its contents along with it,
* whereas moving a container abandons the contents, leaving them behind
* where the container used to be.
*/
class RearSurface: RearContainer
/*
* We're a surface, not a space, so our contents stay attached when
* we move.
*/
abandonLocation = nil
;
/*
* A restricted-contents RearSurface
*/
class RestrictedRearSurface: RestrictedHolder, RearSurface
/* explain the problem */
cannotPutBehindMsg(obj) { return &cannotPutBehindRestrictedMsg; }
/* override PutBehind to enforce our contents restriction */
iobjFor(PutBehind) { check() { checkPutDobj(&cannotPutBehindMsg); } }
;
/* ------------------------------------------------------------------------ */
/*
* A "stretchy container." This is a simple container subclass whose
* external bulk changes according to the bulks of the contents.
*/
class StretchyContainer: Container
/*
* Our minimum bulk. This is the minimum bulk we'll report, even
* when the aggregate bulks of our contents are below this limit.
*/
minBulk = 0
/* get my total external bulk */
getBulk()
{
local tot;
/* start with my own intrinsic bulk */
tot = bulk;
/* add the bulk contribution from my contents */
tot += getBulkForContents();
/* return the total, but never less than the minimum */
return tot >= minBulk ? tot : minBulk;
}
/*
* Calculate the contribution to my external bulk of my contents.
* The default for a stretchy container is to conform exactly to the
* contents, as though the container weren't present at all, hence
* we simply sum the bulks of our contents. Subclasses can override
* this to define other aggregate bulk effects as needed.
*/
getBulkForContents()
{
local tot;
/* sum the bulks of the items in our contents */
tot = 0;
foreach (local cur in contents)
tot += cur.getBulk();
/* return the total */
return tot;
}
/*
* Check what happens when a new object is inserted into my
* contents. This is called with the new object already tentatively
* added to my contents, so we can examine our current status to see
* if everything works.
*
* Since we can change our own size when a new item is added to our
* contents, we'll trigger a full bulk change check.
*/
checkBulkInserted(insertedObj)
{
/*
* inherit the normal handling to ensure that the new object
* fits within this container
*/
inherited(insertedObj);
/*
* since we can change our own shape when items are added to our
* contents, trigger a full bulk check on myself
*/
checkBulkChange();
}
/*
* Check a bulk change of one of my direct contents. Since my own
* bulk changes whenever the bulk of one of my contents changes, we
* must propagate the bulk change of our contents as a change in our
* own bulk.
*/
checkBulkChangeWithin(changingObj)
{
/*
* Do any inherited work, in case we have a limit on our own
* internal bulk.
*/
inherited(changingObj);
/*
* This might cause a change in my own bulk, since my bulk
* depends on the bulks of my contents. When this is called,
* obj is already set to indicate its new bulk; since we
* calculate our own bulk by looking at our contents' bulks,
* this means that our own getBulk will now report the latest
* value including obj's new bulk.
*/
checkBulkChange();
}
;
/* ------------------------------------------------------------------------ */
/*
* "Bag of Holding." This is a mix-in that actively moves items from the
* holding actor's direct inventory into itself when the actor's hands
* are too full.
*
* The bag of holding offers a solution to the conflict between "realism"
* and playability. On the one hand, in real life, you can only hold so
* many items at once, so at first glance it seems a simulation ought to
* have such a limit in order to be more realistic. On the other hand,
* most players justifiably hate having to deal with a carrying limit,
* because it forces the player to spend a lot of time doing tedious
* inventory management.
*
* The Bag of Holding is a compromise solution. The concept is borrowed
* from live role-playing games, where it's usually a magical item that
* can hold objects of unlimited size and weight, thereby allowing
* characters to transport impossibly large objects. In text IF, a bag
* of holding isn't usually magical - it's usually just something like a
* large backpack, or a trenchcoat with lots of pockets. And it usually
* isn't meant as a solution to an obvious puzzle; rather, it's meant to
* invisibly prevent inventory management from becoming a puzzle in the
* first place, by shuffling objects out of the PC's hands automatically
* to free up space as needed.
*
* This Bag of Holding implementation works by automatically moving
* objects from an actor's hands into the bag object, whenever the actor
* needs space to pick up a new item. Whenever an action has a
* "roomToHoldObj" precondition, the precondition will automatically look
* for a BagOfHolding object within the actor's inventory, and then move
* as many items as necessary from the actor's hands to the bag.
*/
class BagOfHolding: object
/*
* Get my bags of holding. Since we are a bag of holding, we'll add
* ourselves to the vector, then we'll inherit the normal handling
* to pick up our contents.
*/
getBagsOfHolding(vec)
{
/* we're a bag of holding */
vec.append(self);
/* inherit the normal handling */
inherited(vec);
}
/*
* Get my "affinity" for the given object. This is an indication of
* how strongly this bag wants to contain the object. The affinity
* is a number in arbitrary units; higher numbers indicate stronger
* affinities. An affinity of zero means that the bag does not want
* to contain the object at all.
*
* The purpose of the affinity is to support specialized holders
* that are designed to hold only specific types of objects, and
* allow these specialized holders to implicitly gather their
* specific objects. For example, a key ring might only hold keys,
* so it would have a high affinity for keys and a zero affinity for
* everything else. A lunchbox might have a higher affinity for
* things like sandwiches than for anything else, but might be
* willing to serve as a general container for other small items as
* well.
*
* The units of affinity are arbitrary, but the library uses the
* following values for its own classes:
*
* 0 - no affinity at all; the bag cannot hold the object
*
* 50 - willing to hold the object, but not of the preferred type
*
* 100 - default affinity; willing and able to hold the object, but
* just as willing to hold most other things
*
* 200 - special affinity; this object is of a type that we
* especially want to hold
*
* We intentionally space these loosely so that games can use
* intermediate levels if desired.
*
* When we are looking for bags of holding to consolidate an actor's
* directly-held inventory, note that we always move the object with
* the highest bag-to-object affinity out of all of the objects
* under consideration. So, if you want to give a particular kind
* of bag priority so that the library uses that bag before any
* other bag, make this routine return a higher affinity for the
* bag's objects than any other bags do.
*
* By default, we'll return the default affinity of 100.
* Specialized bags that don't hold all types of objects must
* override this to return zero for objects they can't hold.
*/
affinityFor(obj)
{
/*
* my affinity for myself is zero, for obvious reasons; for
* everything else, use the default affinity
*/
return (obj == self ? 0 : 100);
}
;
/* ------------------------------------------------------------------------ */
/*
* Keyring - a place to stash keys
*
* Keyrings have some special properties:
*
* - A keyring is a bag of holding with special affinity for keys.
*
* - A keyring can only contain keys.
*
* - Keys are considered to be on the outside of the ring, so a key can
* be used even if attached to the keyring (in other words, if the ring
* itself is held, a key attached to the ring is also considered held).
*
* - If an actor in possession of a keyring executes an "unlock" command
* without specifying what key to use, we will automatically test each
* key on the ring to find the one that works.
*
* - When an actor takes one of our keys, and the actor is in possession
* of this keyring, we'll automatically attach the key to the keyring
* immediately.
*/
class Keyring: BagOfHolding, Thing
/* lister for showing our contents in-line as part of a list entry */
inlineContentsLister = keyringInlineContentsLister
/* lister for showing our contents as part of "examine" */
descContentsLister = keyringExamineContentsLister
/*
* Determine if a key fits our keyring. By default, we will accept
* any object of class Key. However, subclasses might want to
* override this to associate particular keys with particular
* keyrings rather than having a single generic keyring. To allow
* only particular keys onto this keyring, override this routine to
* return true only for the desired keys.
*/
isMyKey(key)
{
/* accept any object of class Key */
return key.ofKind(Key);
}
/* we have high affinity for our keys */
affinityFor(obj)
{
/*
* if the object is one of my keys, we have high affinity;
* otherwise we don't accept it at all
*/
if (isMyKey(obj))
return 200;
else
return 0;
}
/* implicitly put a key on the keyring */
tryPuttingObjInBag(target)
{
/* we're a container, so use "put in" to get the object */
return tryImplicitActionMsg(&announceMoveToBag, PutOn, target, self);
}
/* on taking the keyring, attach any loose keys */
dobjFor(Take)
{
action()
{
/* do the normal work */
inherited();
/* get the list of loose keys */
local lst = getLooseKeys(gActor);
/* consider only the subset that are my valid keys */
lst = lst.subset({x: isMyKey(x)});
/* if there are any, move them onto the keyring */
if (lst.length() != 0)
{
/* put each loose key on the keyring */
foreach (local cur in lst)
cur.moveInto(self);
/* announce what happened */
extraReport(&movedKeysToKeyringMsg, self, lst);
}
}
}
/*
* Get the loose keys in the given actor's possession. On taking the
* keyring, we'll attach these loose keys to the keyring
* automatically. By default, we return any keys the actor is
* directly holding.
*/
getLooseKeys(actor) { return actor.contents; }
/* allow putting a key on the keyring */
iobjFor(PutOn)
{
/* we can only put keys on keyrings */
verify()
{
/* we'll only allow our own keys to be attached */
if (gDobj == nil)
{
/*
* we don't know the actual direct object yet, but we
* can at least check to see if any of the possible
* dobj's is my kind of key
*/
if (gTentativeDobj.indexWhich({x: isMyKey(x.obj_)}) == nil)
illogical(&objNotForKeyringMsg);
}
else if (!isMyKey(gDobj))
{
/* the dobj isn't a valid key for this keyring */
illogical(&objNotForKeyringMsg);
}
}
/* put a key on me */
action()
{
/* move the key into me */
gDobj.moveInto(self);
/* show the default "put on" response */
defaultReport(&okayPutOnMsg);
}
}
/* treat "attach x to keyring" as "put x on keyring" */
iobjFor(AttachTo) remapTo(PutOn, DirectObject, self)
/* treat "detach x from keyring" as "take x from keyring" */
iobjFor(DetachFrom) remapTo(TakeFrom, DirectObject, self)
/* receive notification before an action */
beforeAction()
{
/*
* Note whether or not we want to consider moving the direct
* object to the keyring after a "take" command. We will
* consider doing so only if the direct object isn't already on
* the keyring - if it is, we don't want to move it back right
* after removing it, obviously.
*
* Skip the implicit keyring attachment if the current command
* is implicit, because they must be doing something that
* requires holding the object, in which case taking it is
* incidental. It could be actively annoying to attach the
* object to the keyring in such cases - for example, if the
* command is "put key on keyring," attaching it as part of the
* implicit action would render the explicit command redundant
* and cause it to fail.
*/
moveAfterTake = (!gAction.isImplicit
&& gDobj != nil
&& !gDobj.isDirectlyIn(self));
}
/* flag: consider moving to keyring after this "take" action */
moveAfterTake = nil
/* receive notification after an action */
afterAction()
{
/*
* If the command was "take", and the direct object was a key,
* and the actor involved is holding the keyring and can touch
* it, and the command succeeded in moving the key to the
* actor's direct inventory, then move the key onto the keyring.
* Only consider this if we decided to during the "before"
* notification.
*/
if (moveAfterTake
&& gActionIs(Take)
&& isMyKey(gDobj)
&& isIn(gActor)
&& gActor.canTouch(self)
&& gDobj.isDirectlyIn(gActor))
{
/* move the key to me */
gDobj.moveInto(self);
/*
* Mention what we did. If the only report for this action
* so far is the default 'take' response, then use the
* combined taken-and-attached message. Otherwise, append
* our 'attached' message, which is suitable to use after
* other messages.
*/
if (gTranscript.currentActionHasReport(
{x: (x.ofKind(CommandReportMessage)
&& x.messageProp_ != &okayTakeMsg)}))
{
/*
* we have a non-default message already, so add our
* message indicating that we added the key to the
* keyring
*/
reportAfter(&movedKeyToKeyringMsg, self);
}
else
{
/* use the combination taken-and-attached message */
mainReport(&takenAndMovedToKeyringMsg, self);
}
}
}
/* find among our keys a key that works the direct object */
findWorkingKey(lock)
{
/* try each key on the keyring */
foreach (local key in contents)
{
/*
* if this is the key that unlocks the lock, replace the
* command with 'unlock lock with key'
*/
if (lock.keyFitsLock(key))
{
/* note that we tried keys and found the right one */
extraReport(&foundKeyOnKeyringMsg, self, key);
/* return the key */
return key;
}
}
/* we didn't find the right key - indicate failure */
reportFailure(&foundNoKeyOnKeyringMsg, self);
return nil;
}
/*
* Append my directly-held contents to a vector when I'm directly
* held. We consider all of the keys on the keyring to be
* effectively at the same containment level as the keyring, so if
* the keyring is held, so are its attached keys.
*/
appendHeldContents(vec)
{
/* append all of our contents, since they're held when we are */
vec.appendUnique(contents);
}
/*
* Announce myself as a default object for an action.
*
* Do not announce a keyring as a default for "lock with" or "unlock
* with". Although we can use a keyring as the indirect object of a
* lock/unlock command, we don't actually do the unlocking with the
* keyring; so, when we're chosen as the default, suppress the
* announcement, since it would imply that we're being used to lock
* or unlock something.
*/
announceDefaultObject(whichObj, action, resolvedAllObjects)
{
/* if it's not a lock-with or unlock-with, use the default message */
if (!action.ofKind(LockWithAction)
&& !action.ofKind(UnlockWithAction))
{
/* for anything but our special cases, use the default handling */
return inherited(whichObj, action, resolvedAllObjects);
}
/* use no announcement */
return '';
}
/*
* Allow locking or unlocking an object with a keyring. This will
* automatically try each key on the keyring to see if it fits the
* lock.
*/
iobjFor(LockWith)
{
verify()
{
/* if we don't have any keys, we're not locking anything */
if (contents.length() == 0)
illogical(&cannotLockWithMsg);
/*
* if we know the direct object, and we don't have any keys
* that are plausible for the direct object, we're an
* unlikely match
*/
if (gDobj != nil)
{
local foundPlausibleKey;
/*
* try each of my keys to see if it's plausible for the
* direct object
*/
foundPlausibleKey = nil;
foreach (local cur in contents)
{
/*
* if this is a plausible key, note that we have at
* least one plausible key
*/
if (gDobj.keyIsPlausible(cur))
{
/* note that we found a plausible key */
foundPlausibleKey = true;
/* no need to look any further - one is good enough */
break;
}
}
/*
* If we didn't find a plausible key, we're an unlikely
* match.
*
* If we did find a plausible key, increase the
* likelihood that this is the indirect object so that
* it's greater than the likelihood for any random key
* that's plausible for the lock (which has the default
* likelihood of 100), but less than the likelihood of
* the known good key (which is 150). This will cause a
* keyring to be taken as a default over any ordinary
* key, but will cause the correct key to override the
* keyring as the default if the correct key is known to
* the player already.
*/
if (foundPlausibleKey)
logicalRank(140, 'keyring with plausible key');
else
logicalRank(50, 'no plausible key');
}
}
action()
{
local key;
/*
* Try finding a working key. If we find one, replace the
* command with 'lock <lock> with <key>, so that we have the
* full effect of the 'lock with' command using the key
* itself.
*/
if ((key = findWorkingKey(gDobj)) != nil)
replaceAction(LockWith, gDobj, key);
}
}
iobjFor(UnlockWith)
{
/* verify the same as for LockWith */
verify()
{
/* if we don't have any keys, we're not unlocking anything */
if (contents.length() == 0)
illogical(&cannotUnlockWithMsg);
else
verifyIobjLockWith();
}
action()
{
local key;
/*
* if we can find a working key, run an 'unlock with' action
* using the key
*/
if ((key = findWorkingKey(gDobj)) != nil)
replaceAction(UnlockWith, gDobj, key);
}
}
;
/*
* Key - this is an object that can be used to unlock things, and which
* can be stored on a keyring. The key that unlocks a lock is
* identified with a property on the lock, not on the key.
*/
class Key: Thing
/*
* A key on a keyring that is being held by an actor is considered
* to be held by the actor, since the key does not have to be
* removed from the keyring in order to be manipulated as though it
* were directly held.
*/
isHeldBy(actor)
{
/*
* if I'm on a keyring, I'm being held if the keyring is being
* held; otherwise, use the default definition
*/
if (location != nil && location.ofKind(Keyring))
return location.isHeldBy(actor);
else
return inherited(actor);
}
/*
* Try making the current command's actor hold me. If we're on a
* keyring, we'll simply try to make the keyring itself held, rather
* than taking the key off the keyring; otherwise, we'll inherit the
* default behavior to make ourselves held.
*/
tryHolding()
{
if (location != nil && location.ofKind(Keyring))
return location.tryHolding();
else
return inherited();
}
/* -------------------------------------------------------------------- */
/*
* Action processing
*/
/* treat "detach key" as "take key" if it's on a keyring */
dobjFor(Detach)
{
verify()
{
/* if I'm not on a keyring, there's nothing to detach from */
if (location == nil || !location.ofKind(Keyring))
illogical(&keyNotDetachableMsg);
}
remap()
{
/* if I'm on a keyring, remap to "take self" */
if (location != nil && location.ofKind(Keyring))
return [TakeAction, self];
else
return inherited();
}
}
/* "lock with" */
iobjFor(LockWith)
{
verify()
{
/*
* if we know the direct object is a LockableWithKey, we can
* perform some additional checks on the likelihood of this
* key being the intended key for the lock
*/
if (gDobj != nil
&& gDobj.ofKind(LockableWithKey))
{
/*
* If the player should know that we're the key for the
* lock, boost our likelihood so that we'll be picked
* out automatically from an ambiguous set of keys.
*/
if (gDobj.isKeyKnown(self))
logicalRank(150, 'known key');
/*
* if this isn't a plausible key for the lockable, it's
* unlikely that this is a match
*/
if (!gDobj.keyIsPlausible(self))
illogical(keyNotPlausibleMsg);
}
}
}
/*
* the message to use when the key is obviously not plausible for a
* given lock
*/
keyNotPlausibleMsg = &keyDoesNotFitLockMsg
/* "unlock with" */
iobjFor(UnlockWith)
{
verify()
{
/* use the same key selection we use for "lock with" */
verifyIobjLockWith();
}
}
;
/* ------------------------------------------------------------------------ */
/*
* A Dispenser is a container for a special type of item, such as a book
* of matches or a box of candy.
*/
class Dispenser: Container
/*
* Can we return one of our items to the dispenser once the item is
* dispensed? Books of matches wouldn't generally allow this, since
* a match must be torn out to be removed, but simple box dispensers
* probably would. By default, we won't allow returning an item
* once dispensed.
*/
canReturnItem = nil
/*
* Is the item one of the types of items we dispense? Normally, we
* dispense identical items, so our default implementation simply
* determines if the item is an instance of our dispensable class.
* If the dispenser can hand out items of multiple, unrelated
* classes, this can be overridden to use a different means of
* identifying the dispensed items.
*/
isMyItem(obj) { return obj.ofKind(myItemClass); }
/*
* The class of items we dispense. This is used by the default
* implementation of isMyItem(), so subclasses that inherit that
* implementation should provide the appropriate base class here.
*/
myItemClass = Dispensable
/* "put in" indirect object handler */
iobjFor(PutIn)
{
verify()
{
/* if we know the direct object, consider it further */
if (gDobj != nil)
{
/* if we don't allow returning our items, don't allow it */
if (!canReturnItem && isMyItem(gDobj))
illogical(&cannotReturnToDispenserMsg);
/* if it's not my dispensed item, it can't go in here */
if (!isMyItem(gDobj))
illogical(&cannotPutInDispenserMsg);
}
/* inherit default handling */
inherited();
}
}
;
/*
* A Dispensable is an item that comes from a Dispenser. This is in
* most respects an ordinary item; the only special thing about it is
* that if we're still in our dispenser, we're an unlikely match for any
* command except "take" and the like.
*/
class Dispensable: Thing
/*
* My dispenser. This is usually my initial location, so by default
* we'll pre-initialize this to our location.
*/
myDispenser = nil
/* pre-initialization */
initializeThing()
{
/* inherit the default initialization */
inherited();
/*
* We're usually in our dispenser initially, so assume that our
* dispenser is simply our initial location. If myDispenser is
* overridden in a subclass, don't overwrite the inherited
* value.
*/
if (propType(&myDispenser) == TypeNil)
myDispenser = location;
}
dobjFor(All)
{
verify()
{
/*
* If we're in our dispenser, and the command isn't "take"
* or "take from", reduce our disambiguation likelihood -
* it's more likely that the actor is referring to another
* equivalent item that they've already removed from the
* dispenser.
*/
if (isIn(myDispenser)
&& !gActionIs(Take) && !gActionIs(TakeFrom))
{
/* we're in our dispenser - reduce the likelihood */
logicalRank(60, 'in dispenser');
}
}
}
;
/* ------------------------------------------------------------------------ */
/*
* A Matchbook is a special dispenser for matches.
*/
class Matchbook: Collective, Openable, Dispenser
/* we cannot return a match to a matchbook */
canReturnItem = nil
/*
* we dispense matches (subclasses can override this if they want to
* dispense a specialized match subclass)
*/
myItemClass = Matchstick
/*
* Act as a collective for any items within me. This will have no
* effect unless we also have a plural name that matches that of the
* contained items.
*
* It is usually desirable for a matchbook to act as a collective
* for the contained items, so that a command like "take matches"
* will be taken to apply to the matchbook rather than the
* individual matches.
*/
isCollectiveFor(obj) { return obj.isIn(self); }
/*
* Append my directly-held contents to a vector when I'm directly
* held. When the matchbook is open, append our matches, because we
* consider the matches to be effectively attached to the matchbook
* (rather than contained within it).
*/
appendHeldContents(vec)
{
/* if we're open, append our contents */
if (isOpen)
vec.appendUnique(contents);
}
;
/*
* A FireSource is an object that can set another object on fire. This
* is a mix-in class that can be used with other classes.
*/
class FireSource: object
/*
* We can use a fire source to light another object, provided the
* fire source is itself burning. We don't provide any action
* handling - we leave that to the direct object.
*/
iobjFor(BurnWith)
{
preCond = [objHeld, objBurning]
verify()
{
/* don't allow using me to light myself */
if (gDobj == self)
illogicalNow(&cannotBurnDobjWithMsg);
/*
* If we're already lit, make this an especially good choice
* for lighting other objects - this will ensure that we
* choose this over a match that isn't already lit, which is
* what you'd normally want to do to avoid wasting a match.
*
* Note that our ranking is specifically coordinated with
* that used by Matchstick. We'll use a lit match over any
* normal FireSource (rank 160); we'll use a lit FireSource
* (rank 150) over an unlit match (rank 140).
*
* If we're not lit, make the action non-obvious so that
* we're not taken as a default to light another object on
* fire. We *could* light something once we're lit, but that
* presumes there's a way to light me in the first place,
* which might require yet another object (a match, for
* example) - so ignore me as a default if we're not already
* lit, and go directly to some other object. This should be
* overridden for self-lighting objects such as matches.
*/
if (isLit)
logicalRank(150, 'fire source');
else
nonObvious;
}
}
;
/*
* A Matchstick is a self-igniting match from a matchbook. (We use this
* lengthy name rather than simply "Match" because the latter is too
* generic, and could be taken by a casual reader for an object
* representing a successful search result or the like.)
*/
class Matchstick: FireSource, LightSource
/* matches have fairly feeble light */
brightnessOn = 2
/* not lit initially */
isLit = nil
/* amount of time we burn, in turns */
burnLength = 2
/* default long description describes burning status */
desc()
{
if (isLit)
gLibMessages.litMatchDesc(self);
else
gLibMessages.unlitMatchDesc(self);
}
/* get our state */
getState = (isLit ? matchStateLit : matchStateUnlit)
/* get a list of all states */
allStates = [matchStateLit, matchStateUnlit]
/* "burn" action */
dobjFor(Burn)
{
preCond = [objHeld]
verify()
{
/* can't light a match that's already burning */
if (isLit)
illogicalAlready(&alreadyBurningMsg);
}
action()
{
local t;
/* describe it */
defaultReport(&okayBurnMatchMsg);
/* make myself lit */
makeLit(true);
/* get our default burn length */
t = burnLength;
/*
* if this is an implicit command, reduce the burn length by
* one turn - this ensures that the player can't
* artificially extend the match's useful life by doing
* something that implicitly lights the match
*/
if (gAction.isImplicit)
--t;
/* start our burn-out timer going */
new SenseFuse(self, &matchBurnedOut, t, self, sight);
}
}
iobjFor(BurnWith)
{
verify()
{
/*
* Whether or not a match is burning, it's an especially
* good choice to light something else on fire. Make it
* even more likely when it's burning already.
*
* Note that this is specifically coordinated with the base
* FireSource ranking. We'll pick a lit match (160) over an
* ordinary lit FireSource (150), but we'll pick a lit
* FireSource (150) over an unlit match (140). This will
* avoid consuming a match that's not already lit when
* another fire source is already available.
*/
logicalRank(isLit ? 160 : 140, 'fire source');
}
}
/* "extinguish" */
dobjFor(Extinguish)
{
verify()
{
/* can't extinguish a match that isn't burning */
if (!isLit)
illogicalAlready(&matchNotLitMsg);
}
action()
{
/* describe the match going out */
defaultReport(&okayExtinguishMatchMsg);
/* no longer lit */
makeLit(nil);
/* remove the match from the game */
moveInto(nil);
}
}
/* fuse handler for burning out */
matchBurnedOut()
{
/*
* if I'm not still burning, I must have been extinguished
* explicitly already, so there's nothing to do
*/
if (!isLit)
return;
/* make sure we separate any output from other commands */
"<.p>";
/* report that we're done burning */
gLibMessages.matchBurnedOut(self);
/*
* remove myself from the game (for simplicity, a match simply
* disappears when it's done burning)
*/
moveInto(nil);
}
/* matches usually come in bunches of equivalents */
isEquivalent = true
;
/*
* A light source that produces light using a fuel supply. This kind of
* light source uses a daemon to consume fuel whenever it's lit.
*/
class FueledLightSource: LightSource
/* provide a bright light by default */
brightnessOn = 3
/* not lit initially */
isLit = nil
/*
* Our fuel source object. If desired, this can be set to a
* separate object to model the fuel supply separately from the
* light source itself; for example, you could set this to point to
* a battery, or to a vial of oil. By default, for simplicity, the
* fuel supply and light source are the same object.
*
* The fuel supply object must expose two methods: getFuelLevel()
* and consumeFuel().
*/
fuelSource = (self)
/*
* Get my fuel level, and consume fuel. We use these methods only
* when we're our own fuelSource (which we are by default). When
* we're not our own fuel source, the fuel source object must
* provide these methods instead of us.
*
* Our fuel level is the number of turns that we can continue to
* burn. Each turn we're lit, we'll reduce the fuel level by one.
* We'll automatically extinguish ourself when the fuel level
* reaches zero.
*
* If the light source can burn forever, simply return nil as the
* fuel level.
*/
getFuelLevel() { return fuelLevel; }
consumeFuel(amount) { fuelLevel -= amount; }
/* our fuel level - we use this when we're our own fuel source */
fuelLevel = 20
/* light or extinguish */
makeLit(lit)
{
/* if the current fuel level is zero, we can't be lit */
if (lit && fuelSource.getFuelLevel() == 0)
return;
/* inherit the default handling */
inherited(lit);
/* if we're lit, activate our daemon; otherwise, stop our daemon */
if (isLit)
{
/* start our burn daemon going */
burnDaemonObj =
new SenseDaemon(self, &burnDaemon, 1, self, sight);
}
else
{
/* stop our daemon */
eventManager.removeEvent(burnDaemonObj);
/* forget out daemon */
burnDaemonObj = nil;
}
}
/* burn daemon - this is called on each turn while we're burning */
burnDaemon()
{
local level = fuelSource.getFuelLevel();
/* if we use fuel, consume one increment of fuel for this turn */
if (level != nil)
{
/*
* If our fuel level has reached zero, stop burning. Note
* that the daemon is called on the first turn after we
* start burning, so we must go through a turn with the fuel
* level at zero before we stop burning.
*/
if (level == 0)
{
/* make sure we separate any output from other commands */
"<.p>";
/* mention that the candle goes out */
sayBurnedOut();
/*
* Extinguish the candle. Note that we do this *after*
* we've already displayed the message about the candle
* burning out, because that message is displayed in our
* own sight context. If we're the only light source
* present, then we're invisible once we're not providing
* light, so our message about burning out would be
* suppressed if we displayed it after cutting off our
* own light. To make sure we can see the message, wait
* until after the message to cut off our light.
*/
makeLit(nil);
}
else
{
/* reduce our fuel level by one */
fuelSource.consumeFuel(1);
}
}
}
/* mention that we've just burned out */
sayBurnedOut() { gLibMessages.objBurnedOut(self); }
/* our daemon object, valid while we're burning */
burnDaemonObj = nil
;
/*
* A candle is an item that can be set on fire for a controlled burn.
* Although we call this a candle, this class can be used for other types
* of fuel burners, such as torches and oil lanterns.
*
* Ordinary candles are usually fire sources as well, in that you can
* light one candle with another once the first one is lit. To get this
* effect, mix FireSource into the superclass list (but put it before
* Candle, since FireSource is specifically designed as a mix-in class).
*/
class Candle: FueledLightSource
/*
* The message we display when we try to light the candle and we're
* out of fuel. This message can be overridden by subclasses that
* don't fit the default message.
*/
outOfFuelMsg = &candleOutOfFuelMsg
/* the message we display when we successfully light the candle */
okayBurnMsg = &okayBurnCandleMsg
/* show a message when the candle runs out fuel while burning */
sayBurnedOut()
{
/* by default, show our standard library message */
gLibMessages.candleBurnedOut(self);
}
/*
* Determine if I can be lit with the specific indirect object. By
* default, we'll allow any object to light us if the object passes
* the normal checks applied by its own iobjFor(BurnWith) handlers.
* This can be overridden if we can only be lit with specific
* sources of fire; for example, a furnace with a deeply-recessed
* burner could refuse to be lit by anything but particular long
* matches, or a particular type of fuel could refuse to be lit
* except by certain especially hot flames.
*/
canLightWith(obj) { return true; }
/*
* Default long description describes burning status. In most
* cases, this should be overridden to provide more details, such as
* information on our fuel level.
*/
desc()
{
if (isLit)
gLibMessages.litCandleDesc(self);
else
inherited();
}
/* "burn with" action */
dobjFor(BurnWith)
{
preCond = [touchObj]
verify()
{
/* can't light it if it's already lit */
if (isLit)
illogicalAlready(&alreadyBurningMsg);
}
check()
{
/*
* make sure the object being used to light us is a valid
* source of fire for us
*/
if (!canLightWith(obj))
{
reportFailure(&cannotBurnDobjWithMsg);
exit;
}
/* if the fuel level is zero, we can't be lit */
if (fuelSource.getFuelLevel() == 0)
{
reportFailure(outOfFuelMsg);
exit;
}
}
action()
{
/* make myself lit */
makeLit(true);
/* describe it */
defaultReport(okayBurnMsg);
}
}
/* "extinguish" */
dobjFor(Extinguish)
{
verify()
{
/* can't extinguish a match that isn't burning */
if (!isLit)
illogicalAlready(&candleNotLitMsg);
}
action()
{
/* describe the match going out */
defaultReport(&okayExtinguishCandleMsg);
/* no longer lit */
makeLit(nil);
}
}
;
/* ------------------------------------------------------------------------ */
/*
* "Tour Guide" is a mix-in class for Actors. This class can be
* multiply inherited by objects along with Actor or a subclass of
* Actor. This mix-in makes the Follow action, when applied to the tour
* guide, initiate travel according to where the tour guide wants to go
* next. So, if the tour guide is here and is waving us through the
* door, FOLLOW GUIDE will initiate travel through the door.
*
* This class should appear in the superclass list ahead of Actor or the
* Actor subclass.
*/
class TourGuide: object
dobjFor(Follow)
{
verify()
{
/*
* If the actor can see us, and we're in a "guided tour"
* state, we can definitely perform the travel. Otherwise,
* use the standard "follow" behavior.
*/
if (gActor.canSee(self) && getTourDest() != nil)
{
/*
* we're waiting to show the actor to the next stop on
* the tour, so we can definitely proceed with this
* action
*/
}
else
{
/* we're not in a tour state, so use the standard handling */
inherited();
}
}
action()
{
local dest;
/*
* if we're in a guided tour state, initiate travel to our
* escort destination; otherwise, use the standard handling
*/
if (gActor.canSee(self) && (dest = getTourDest()) != nil)
{
/* initiate travel to our destination */
replaceAction(TravelVia, dest);
return;
}
else
{
/* no tour state; use the standard handling */
inherited();
}
}
}
/*
* Get the travel connector that takes us to our next guided tour
* destination. By default, this returns the escortDest from our
* current actor state if our state is a guided tour state, or nil
* if our state is any other kind of state. Subclasses must
* override this if they use other kinds of states to represent
* guided tours, since we'll only detect that we're in a guided tour
* state if our current actor state object is of class
* GuidedTourState (or any subclass).
*/
getTourDest()
{
return (curState.ofKind(GuidedTourState)
? curState.escortDest
: nil);
}
;
/*
* Guided Tour state. This provides a simple way of defining a "guided
* tour," which is a series of locations to which we try to guide the
* player character. We don't force the player character to travel as
* specified; we merely try to lead the player. The actual travel is up
* to the player.
*
* Here's how this works. For each location on the guided tour, create
* one of these state objects. Set escortDest to the travel connector
* to which we're attempting to guide the player character from the
* current location. Set stateAfterEscort to the state object for the
* next location on the tour. Set stateDesc to something indicating
* that we're trying to show the player to the next stop - something
* along the lines of "Bob waits for you by the door." Set
* arrivingWithDesc to a message indicating that we just showed up in
* the current location and are ready to show the player to the next -
* "Bob goes to the door and waits for you to follow him."
*/
class GuidedTourState: AccompanyingState
/* the travel connector we're trying to show the player into */
escortDest = nil
/*
* The next state for our actor to assume after the travel. This
* should be overridden and set to the state object for the next
* stop on the tour.
*/
stateAfterEscort = nil
/* the actor we're escorting - this is usually the player character */
escortActor = (gPlayerChar)
/*
* The class we use for our actor state during the escort travel.
* By default, we use the basic guided-tour accompanying travel
* state class, but games will probably want to use a customized
* subclass of this basic class in most cases. The main reason to
* use a custom subclass is to provide customized messages to
* describe the departure of the escorting actor.
*/
escortStateClass = GuidedInTravelState
/*
* we should accompany the travel if the actor we're guiding will be
* traveling, and they're traveling to the next stop on our tour
*/
accompanyTravel(traveler, conn)
{
return (traveler.isActorTraveling(escortActor) && conn == escortDest);
}
/*
* get our accompanying state object - we'll create an instance of
* the class specified in our escortStateClass property
*/
getAccompanyingTravelState(traveler, conn)
{
return escortStateClass.createInstance(
location, gActor, stateAfterEscort);
}
;
/*
* A subclass of the basic accompanying travel state specifically
* designed for guided tours. This is almost the same as the basic
* accompanying travel state, but provides customized messages to
* describe the departure of our associated actor, which is the actor
* serving as the tour guide.
*/
class GuidedInTravelState: AccompanyingInTravelState
sayDeparting(conn)
{ gLibMessages.sayDepartingWithGuide(location, leadActor); }
;
/* ------------------------------------------------------------------------ */
/*
* An Attachable is an object that can be attached to another, using an
* ATTACH X TO Y command. This is a mix-in class that is meant to be
* combined with a Thing-derived class to create an attachable object.
*
* Attachment is symmetrical: we can only attach to other Attachable
* objects. As a result, the verb handling for ATTACH can be performed
* symmetrically - ATTACH X TO Y is handled the same way as ATTACH Y TO
* X. Sometimes reversing the roles makes the command nonsensical, but
* when the reversal makes sense, it seems unlikely that it'll ever
* change the meaning of the command. This makes it program the verb
* handling, because it means that we can designate one of X or Y as the
* handler for the verb, and just write the code once there. Refer to
* the handleAttach() method to see how this works.
*
* There's an important detail that we leave to instances, because
* there's no good general rule we can implement. Specifically, there's
* the matter of imposing appropriate constraints on the relative
* locations of objects once they're attached to one another. There are
* numerous anomalies that become possible once two objects are attached.
* Consider the example of a battery connected to a jumper cable that's
* in turn connected to a lamp:
*
* - if we put the battery in a box but leave the lamp outside the box,
* we shouldn't be able to close the lid of the box all the way without
* breaking the cables
*
* - if we're carrying the battery but not the lamp, traveling to a new
* room should drag the lamp along
*
* - if we drop the battery down a well, the lamp should be dragged down
* with it
*
* Our world model isn't sophisticated enough to properly model an
* attachment relationship, so it can't deal with these contingencies by
* proper physical simulation. Which is why we have to leave these for
* the game to handle.
*
* There are two main strategies you can apply to handle these problems.
*
* First, you can impose limits that prevent these sorts of situations
* from coming up in the first place, either by carefully designing the
* scenario so they simply don't come up, or by imposing more or less
* artificial constraints. For example, you could solve all of the
* problems above by eliminating the jumper cable and attaching the lamp
* directly to the battery, or by making the jumper cable very short.
* Anything attached to the battery would effectively become located "in"
* the battery, so it would move everywhere along with the battery
* automatically. Detaching the lamp would move the lamp back outside
* the battery, and conversely, moving the lamp out of the battery would
* detach the objects.
*
* Second, you can detect the anomalous cases and handle them explicitly
* with special-purpose code. You could use beforeAction and afterAction
* methods on one of the attached objects, for example, to detect the
* various problematic actions, either blocking them or implementing
* appropriate consequences.
*
* Given the number of difficult anomalies possible with rope-like
* objects, the second approach is challenging on its own. However, it
* often helps to combine it with the first approach, limiting the
* scenario. In other words, you'd limit the scenario to some extent,
* but not totally: rather than completely excising the difficult
* behavior, you'd narrow it down to a manageable subset of the full
* range of real-world possibilities; then, you'd deal with the remaining
* anomalies on a case-by-case basis. For example, you could make the
* battery too heavy to carry, which would guarantee that it would never
* be put in a box, thrown down a well, or carried out of the room. That
* would only leave a few issues: walking away while carrying the plugged
* in lamp, which could be handled with an afterAction that severs the
* attachment; putting the lamp in a box and closing the box, which could
* be handled with a beforeAction by blocking Close actions whenever the
* lamp is inside the object being closed.
*/
class Attachable: object
/*
* The list of objects I'm currently attached to. Note that each of
* the objects in this list must usually be an Attachable, and we
* must be included in the attachedObjects list in each of these
* objects.
*/
attachedObjects = []
/*
* Perform programmatic attachment, without any notifications. This
* simply updates my attachedObjects list and the other object's list
* to indicate that we're attached to the other object (and vice
* versa).
*/
attachTo(obj)
{
attachedObjects += obj;
obj.attachedObjects += self;
}
/* perform programmatic detachment, without any notifications */
detachFrom(obj)
{
attachedObjects -= obj;
obj.attachedObjects -= self;
}
/* get the subset of my attachments that are non-permanent */
getNonPermanentAttachments()
{
/* return the subset of objects not permanently attached */
return attachedObjects.subset({x: !isPermanentlyAttachedTo(x)});
}
/* am I attached to the given object? */
isAttachedTo(obj)
{
/* we are attached to the other object if it's in our list */
return (attachedObjects.indexOf(obj) != nil);
}
/*
* Am I the "major" item in my attachment relationship to the given
* object? This affects how our relationship is described in our
* status message: in an asymmetrical relationship, where one object
* is the "major" item, we will always describe the minor item as
* being attached to the major item rather than vice versa. This
* allows you to ensure that the message is always "the sign is
* attached to the wall", and never "the wall is attached to the
* sign": the wall is the major item in this relationship, so it's
* always the sign that's attached to it.
*
* By default, we always return nil here, which means that
* attachment relationships are symmetrical by default. In a
* symmetrical relationship, we'll describe the other things as
* attached to 'self' when describing self.
*/
isMajorItemFor(obj) { return nil; }
/*
* Am I *listed* as attached to the given object? If this is true,
* then our examineStatus() will list 'obj' among the things I'm
* attached to: "Self is attached to obj." If this is nil, I'm not
* listed as attached.
*
* By default, we're listed if (1) we're not permanently attached to
* 'obj', AND (2) we're not the "major" item in the attachment
* relationship. The reason we're not listed if we're permanently
* attached is that the attachment information is presumably better
* handled via the fixed description of the object rather than in
* the extra status message; this is analogous to the way immovable
* items (such as Fixtures) aren't normally listed in the
* description of a room. The reason we're not listed if we're the
* "major" item in the relationship is that the "major" status
* reverses the relationship: when we're the major item, the other
* item is described as attached to *us*, rather than vice versa.
*/
isListedAsAttachedTo(obj)
{
/*
* only list the item if it's not permanently attached, and
* we're not the "major" item for the object
*/
return (!isPermanentlyAttachedTo(obj) && !isMajorItemFor(obj));
}
/*
* Is 'obj' listed as attached to me when I'm described? If this is
* true, then our examineStatus() will list 'obj' among the things
* attached to me: "Attached to self is obj." If this is nil, then
* 'obj' is not listed among the things attached to me when I'm
* described.
*
* This routine is simply the "major" list counterpart of
* isListedAsAttachedTo().
*
* By default, we list 'obj' among my attachments if (1) I'm the
* "major" item for 'obj', AND (2) 'obj' is listed as attached to
* me, as indicated by obj.isListedAsAttachedTo(self). We only list
* our minor attachments here, because we list all of our other
* listable attachments separately, as the things I'm attached to.
* We also only list items that are themselves listable as
* attachments, for obvious reasons.
*/
isListedAsMajorFor(obj)
{
/*
* only list the item if we're the "major" item for the object,
* and the object is itself listable as an attachment
*/
return (isMajorItemFor(obj) && obj.isListedAsAttachedTo(self));
}
/*
* Can I attach to the given object? This returns true if the other
* object is allowable as an attachment, nil if not.
*
* By default, we look to see if the other side is an Attachable, and
* if so, if it overrides canAttachTo(); if so, we'll call its
* canAttachTo to ask whether it thinks it can attach to us. If the
* other side doesn't override this, we'll simply return nil. This
* arrangement is convenient because it means that only one side of
* an attachable pair needs to implement this; the other side will
* automatically figure it out by calling the first side and relying
* on the symmetry of the relationship.
*/
canAttachTo(obj)
{
/*
* if the other side's an Attachable, and it overrides this
* method, call the override; if not, it's by default not one of
* our valid attachments
*/
if (overrides(obj, Attachable, &canAttachTo))
{
/*
* the other side is an Attachable that defines a specific
* attachment rule, so ask the other side if it thinks we're
* one of its attachments; by the symmetry of the
* relationship, if we're one of its attachments, then it's
* one of ours
*/
return obj.canAttachTo(self);
}
else
{
/*
* the other side doesn't want to tell us, so we're on our
* own; we don't recognize any attachments on our own, so
* it's not a valid attachment
*/
return nil;
}
}
/*
* Explain why we can't attach to the given object. This should
* simply display an appropriate mesage. We use reportFailure to
* flag it as a failure report, but that's not actually required,
* since we call this from our 'check' routine, which will mark the
* action as having failed even if we don't here.
*/
explainCannotAttachTo(obj) { reportFailure(&wrongAttachmentMsg); }
/*
* Is it possible for me to detach from the given object? This asks
* whether a given attachment relationship can be dissolved with
* DETACH FROM.
*
* By default, we'll use similar logic to canAttachTo: if the other
* object overrides canDetachFrom(), we'll let it make the
* determination. Otherwise, we'll return nil if one or the other
* side is a PermanentAttachment, true if not. This lets you prevent
* detachment by overriding canDetachFrom() on just one side of the
* relationship.
*/
canDetachFrom(obj)
{
/* if the other object overrides canDetachFrom, defer to it */
if (overrides(obj, Attachable, &canDetachFrom))
{
/* let the other side make the judgment */
return obj.canDetachFrom(self);
}
else
{
/*
* the other side doesn't override it, so assume we can
* detach unless one or the other side is a
* PermanentAttachment
*/
return !isPermanentlyAttachedTo(obj);
}
}
/*
* Am I permanently attached to the other object? This returns true
* if I'm a PermanentAttachment or the other object is.
*/
isPermanentlyAttachedTo(obj)
{
/*
* if either one of us is a PermanentAttachment, we're
* permanently attached to each other
*/
return ofKind(PermanentAttachment) || obj.ofKind(PermanentAttachment);
}
/*
* A message explaining why we can't detach from the given object.
* Note that 'obj' can be nil, because we could be attempting a
* DETACH command with no indirect object.
*/
cannotDetachMsgFor(obj)
{
/*
* if we have an object, it must be the wrong one; otherwise, we
* simply can't detach generically, since the object to detach
* from wasn't specified, and there's nothing obvious we can
* detach from
*/
return obj != nil ? &wrongDetachmentMsg : &cannotDetachMsg;
}
/*
* Process attachment to a new object. This routine is called on
* BOTH the direct and indirect object during the attachment process
* - that is, it's called on the direct object with the indirect
* object as the argument, and then it's called on the indirect
* object with the direct object as the argument.
*
* This symmetrical handling makes it easy to handle the frequent
* cases where the player might say ATTACH X TO Y or ATTACH Y TO X
* and mean the same thing either way. Because this method is called
* for both X and Y in either phrasing, you can simply choose to
* write the handler code in either X or Y - you only have to write
* it once, because the handler will be called on each of the
* objects, regardless of the phrasing. So, if you choose to
* designate X as the official ATTACH handler, write a handleAttach()
* method on X, and leave the one on Y doing nothing: during
* execution, the X method will do its work, and the Y method will do
* nothing, so regardless of phrasing order, the net result will be
* the same.
*
* By default we do nothing. Each instance should override this to
* display any extra message and take any extra action needed to
* process the attachment status change. Note that the override
* doesn't need to worry about managing the attachedObjects list, as
* the main action handler does that automatically.
*
* Note that handleAttach() is always called after both objects have
* updated their attachedObjects lists. This means that you can turn
* right around and detach the objects here, if you don't want to
* leave them attached.
*/
handleAttach(other)
{
/* do nothing by default */
}
/*
* Receive notification that this object or one of its attachments
* is being moved to a new container. When an attached object is
* moved, we'll call this on the object being moved AND on every
* object attached to it. 'movedObj' is the object being moved, and
* 'newCont' is the new container it's being moved into.
*
* By default we do nothing. Instances can override this as needed.
* For example, if you wish to enforce a rule that this object and
* all of its attached objects share a common direct container, you
* could either block the move (by displaying an error and using
* 'exit') or run a nested DetachFrom action to sever the attachment
* with the object being moved.
*/
moveWhileAttached(movedObj, newCont)
{
/* do nothing by default */
}
/*
* Receive notification that this object or one of its attachments is
* being moved in the course of an actor traveling to a new location.
* Whenever anyone travels while carrying an attachable object
* (directly or indirectly), we'll call this on the object being
* moved AND on every object attached to it. 'movedObj' is the
* object being carried by the traveling actor, 'traveler' is the
* Traveler performing the travel, and 'connector' is the
* TravelConnector that the traveler is traversing.
*
* By default, we do nothing. Instances can override this as needed.
*/
travelWhileAttached(movedObj, traveler, connector)
{
/* do nothing by default */
}
/*
* Handle detachment. This works like handleAttach(), in that this
* routine is invoked symmetrically for both sides of a DETACH X FROM
* Y commands.
*
* As with handleAttach(), we do nothing by default, so instances
* should override as needed. Note that the override doesn't need to
* worry about managing the attachedObjects list, as the main action
* handler does that automatically. As with handleAttach(), this is
* called after the attachedObjects lists for both objects are
* updated.
*/
handleDetach(other)
{
/* do nothing by default */
}
/* the Lister we use to show our list of attached objects */
attachmentLister = perInstance(new SimpleAttachmentLister(self))
/*
* the Lister we use to list the items attached to us (i.e., the
* items for which we're the "major" item in the attachment
* relationship)
*/
majorAttachmentLister = perInstance(new MajorAttachmentLister(self))
/* add a list of our attachments to the desription */
examineStatus()
{
local tab;
/* inherit the normal status description */
inherited();
/* get the actor's visual sense table */
tab = gActor.visibleInfoTable();
/* add our list of attachments */
attachmentLister.showList(gActor, self, attachedObjects,
0, 0, tab, nil);
/* add our list of major attachments */
majorAttachmentLister.showList(gActor, self, attachedObjects,
0, 0, tab, nil);
}
/*
* Move into a new container. If I'm attached to anything, we'll
* notify ourself and our attachments.
*/
mainMoveInto(newCont)
{
/* if I'm attached to anything, notify everyone */
if (attachedObjects.length() != 0)
{
/* notify myself */
moveWhileAttached(self, newCont);
/* notify my attachments */
attachedObjects.forEach({x: x.moveWhileAttached(self, newCont)});
}
/* inherit the base handling */
inherited(newCont);
}
/*
* Receive notification of travel. If I'm involved in the travel,
* and I'm attached to anything, we'll notify ourself and our
* attachments.
*/
beforeTravel(traveler, connector)
{
/*
* If we're traveling with the traveler, and we're attached to
* anything, notify everything that's attached.
*/
if (attachedObjects.length() != 0
&& traveler.isTravelerCarrying(self))
{
/* notify myself */
travelWhileAttached(self, traveler, connector);
/* notify each of my attachments */
attachedObjects.forEach(
{x: x.travelWhileAttached(self, traveler, connector)});
}
}
/*
* during initialization, make sure the attachedObjects list is
* symmetrical for both sides of the attachment relationship
*/
initializeThing()
{
/* do the normal work */
inherited();
/*
* check to make sure that each of our attached objects points
* back at us
*/
foreach (local cur in attachedObjects)
{
/*
* if we're not in this one's attachedObjects list, add
* ourselves to the list, so that everyone's consistent
*/
if (cur.attachedObjects.indexOf(self) == nil)
cur.attachedObjects += self;
}
}
/* handle attachment on the direct object side */
dobjFor(AttachTo)
{
/* require that the actor can touch the direct object */
preCond = [touchObj]
verify()
{
/*
* it makes sense to attach to anything but myself, or things
* we're already attached to
*/
if (gIobj != nil)
{
if (isAttachedTo(gIobj))
illogicalAlready(&alreadyAttachedMsg);
else if (gIobj == self)
illogicalSelf(&cannotAttachToSelfMsg);
}
}
check()
{
/* only allow it if we can attach to the other object */
if (!canAttachTo(gIobj))
{
explainCannotAttachTo(gIobj);
exit;
}
}
action()
{
/* add the other object to our list of attached objects */
attachedObjects += gIobj;
/* add our default acknowledgment */
defaultReport(&okayAttachToMsg);
/* fire the handleAttach event if we're ready */
maybeHandleAttach(gIobj);
}
}
/* handle attachment on the indirect object side */
iobjFor(AttachTo)
{
/*
* Require that the direct object can touch the indirect object.
* This ensures that the two objects to be attached can touch
* one another. Note that we don't also require that the actor
* be able to touch the indirect object directly, since it's
* good enough that (1) the actor can touch the direct object
* (which we enforce with the dobj precondition), and (2) the
* direct object can touch the indirect object. This allows for
* odd things like plugging something into a recessed outlet,
* where the recessed bit can't be reached directly but can be
* reached using the plug.
*/
preCond = [dobjTouchObj]
verify()
{
/*
* it makes sense to attach to anything but myself, or things
* we're already attached to
*/
if (gDobj != nil)
{
if (isAttachedTo(gDobj))
illogicalAlready(&alreadyAttachedMsg);
else if (gDobj == self)
illogicalSelf(&cannotAttachToSelfMsg);
}
}
check()
{
/* only allow it if we can attach to the other object */
if (!canAttachTo(gDobj))
{
explainCannotAttachTo(gDobj);
exit;
}
}
action()
{
/* add the other object to our list of attached objects */
attachedObjects += gDobj;
/* fire the handleAttach event if we're ready */
maybeHandleAttach(gIobj);
}
}
/*
* Fire the handleAttach event - we'll notify both sides as soon as
* both sides are hooked up with each other. This ensures that both
* lists are updated before we notify either side, so the ordering
* doesn't depend on whether we handle the dobj or iobj first.
*/
maybeHandleAttach(other)
{
/* if both lists are hooked up, send the notifications */
if (attachedObjects.indexOf(other) != nil
&& other.attachedObjects.indexOf(self) != nil)
{
/* notify our side */
handleAttach(other);
/* notify the other side */
other.handleAttach(self);
}
}
/* handle simple, unspecified detachment (DETACH OBJECT) */
dobjFor(Detach)
{
verify()
{
/* if I'm not attached to anything, this is illogical */
if (attachedObjects.length() == 0)
illogicalAlready(cannotDetachMsgFor(nil));
}
action()
{
local lst;
/* get the non-permanent attachment subset */
lst = getNonPermanentAttachments();
/* check what that leaves us */
if (lst.length() == 0)
{
/*
* we're not attached to anything that we can detach
* from, so simply report that we can't detach
* generically
*/
reportFailure(cannotDetachMsgFor(nil));
}
else if (lst.length() == 1)
{
/*
* we have exactly one attached object from which we can
* detach, so they must want to detach from that -
* process this as DETACH FROM my one attached object
*/
replaceAction(DetachFrom, self, lst[1]);
}
else
{
/*
* we have more than one detachable attachment, so ask
* which one they mean
*/
askForIobj(DetachFrom);
}
}
}
/* handle detaching me from a specific other object */
dobjFor(DetachFrom)
{
verify()
{
/* it only makes sense to try detaching us from our attachments */
if (gIobj != nil && !isAttachedTo(gIobj))
illogicalAlready(¬AttachedToMsg);
}
check()
{
/* make sure I'm allowed to detach from the given object */
if (!canDetachFrom(gIobj))
{
reportFailure(cannotDetachMsgFor(gIobj));
exit;
}
}
action()
{
/* remove the other object from our list of attached objects */
attachedObjects -= gIobj;
/* add our default acknowledgment */
defaultReport(&okayDetachFromMsg);
/* fire the handleDetach event if appropriate */
maybeHandleDetach(gIobj);
}
}
/* handle detachment on the indirect object side */
iobjFor(DetachFrom)
{
verify()
{
/* it only makes sense to try detaching my attachments */
if (gDobj == nil)
{
/*
* we don't know the dobj yet, but we can check the
* tentative list for the possible set
*/
if (gTentativeDobj
.indexWhich({x: isAttachedTo(x.obj_)}) == nil)
illogicalAlready(¬AttachedToMsg);
}
else if (gDobj != nil && !isAttachedTo(gDobj))
illogicalAlready(¬AttachedToMsg);
}
check()
{
/* make sure I'm allowed to detach from the given object */
if (!canDetachFrom(gDobj))
{
reportFailure(cannotDetachMsgFor(gDobj));
exit;
}
}
action()
{
/* remove the other object from our list of attached objects */
attachedObjects -= gDobj;
/* fire the handleDetach event if appropriate */
maybeHandleDetach(gDobj);
}
}
/*
* Fire the handleDetach event - we'll notify both sides as soon as
* both sides are un-hooked up. This ensures that both lists are
* updated before we notify either side, so the ordering doesn't
* depend on whether we handle the dobj or iobj first.
*/
maybeHandleDetach(other)
{
/* if both lists are un-hooked up, send the notifications */
if (attachedObjects.indexOf(other) == nil
&& other.attachedObjects.indexOf(self) == nil)
{
/* notify our side */
handleDetach(other);
/* notify the other side */
other.handleDetach(self);
}
}
/*
* TAKE X FROM Y is the same as DETACH X FROM Y for things we're
* attached to, but use the inherited handling otherwise
*/
dobjFor(TakeFrom)
{
verify()
{
/*
* use the inherited handling only if we're not attached -
* if we're attached, consider it logical, overriding any
* containment relationship check we might otherwise make
*/
if (gIobj == nil || !isAttachedTo(gIobj))
inherited();
}
check()
{
/* inherit the default check only if we're not attached */
if (!isAttachedTo(gIobj))
inherited();
}
action()
{
/*
* if we're attached, change this into a DETACH FROM action;
* otherwise, use the inherited TAKE FROM handling
*/
if (isAttachedTo(gIobj))
replaceAction(DetachFrom, self, gIobj);
else
inherited();
}
}
iobjFor(TakeFrom)
{
verify()
{
/* use the inherited handling only if we're not attached */
if (gDobj == nil || !isAttachedTo(gDobj))
inherited();
}
check()
{
/* inherit the default check only if we're not attached */
if (!isAttachedTo(gDobj))
inherited();
}
action()
{
/* inherit the default action only if we're not attached */
if (!isAttachedTo(gDobj))
inherited();
}
}
;
/*
* An Attachable-specific precondition: the Attachable isn't already
* attached to something else. This can be added to the preCond list for
* an Attachable (for iobjFor(AttachTo) and dobjFor(AttachTo)) to ensure
* that any existing attachment is removed before a new attachment is
* formed. This is useful when the Attachable can connect to only one
* thing at a time.
*/
objNotAttached: PreCondition
checkPreCondition(obj, allowImplicit)
{
/*
* if we don't already have any non-permanent attachments, we're
* fine (as we don't require removing permanent attachments)
*/
if (obj.attachedObjects.indexWhich(
{x: !obj.isPermanentlyAttachedTo(x)}) == nil)
return nil;
/*
* Try an implicit Detach command. It should be safe to use the
* form that doesn't specify what we're detaching from, since the
* whole point of this condition is that the object can have only
* one non-permanent attachment, hence the vague Detach handler
* should be able to figure out what we mean.
*/
if (allowImplicit && tryImplicitAction(Detach, obj))
{
/* if we're still attached to anything, we failed, so abort */
if (obj.attachedObjects.indexWhich(
{x: !obj.isPermanentlyAttachedTo(x)}) != nil)
exit;
/* tell the caller we executed an implied action */
return true;
}
/* we must detach first */
reportFailure(&mustDetachMsg, obj);
exit;
}
;
/*
* A "nearby" attachable is a subclass of Attachable that adds a
* requirement that the attached objects be in a given location. By
* default, we simply require that they have a common immediate
* container, but this can be overridden so that each object's location
* is negotiated separately. This is a simple and effective pattern that
* avoids many of the potential anomalies with attachment (see the
* Attachable comments for examples).
*
* In AttachTo actions, we enforce the nearby requirement with a
* precondition requiring the direct object to be in the same immediate
* container as the indirect object, and vice versa. In
* moveWhileAttached(), we enforce the rule by detaching the objects if
* one is being moved away from the other's immediate container.
*/
class NearbyAttachable: Attachable
dobjFor(AttachTo)
{
/* require that the objects be in the negotiated locations */
preCond = (inherited() + nearbyAttachableCond)
}
iobjFor(AttachTo)
{
/* require that the objects be in the negotiated locations */
preCond = (inherited() + nearbyAttachableCond)
}
/*
* Get the target locations for attaching to the given other object.
* The "target locations" are the locations where the objects are
* required to be in order to carry out the ATTACH command to attach
* this object to the other object (or vice versa).
*
* This method returns a list with three elements. The first
* element is the target location for 'self', and the second is the
* target location for 'other', the object we're attaching to. The
* third element is an integer giving the priority; a higher number
* means higher priority.
*
* The priority is an arbitrary value that we use to determine which
* of the two objects involved in the attach gets to decide on the
* target locations. We call this method on both of the two objects
* being attached to one another, then we use the target locations
* returned by the object that claims the higher priority. If the
* two priorities are equal, we pick one arbitrarily.
*
* The default implementation chooses my own immediate container as
* the target location for both objects. However, if the other
* object is non-portable, we'll choose its immediate location
* instead, since we obviously can't move it to our container.
*/
getNearbyAttachmentLocs(other)
{
/*
* If the other object is portable, use our immediate container
* as the proposed location for both objects; otherwise, use the
* other object's immediate container. In any case, use a low
* priority, since we're just the default base class
* implementation; any override will generally have higher
* priority.
*/
if (other.ofKind(NonPortable))
{
/* the other can't be moved, so use its location */
return [other.location, other.location, 0];
}
else
{
/*
* the other can be moved, so use our own location, in a
* paraphrase of the realty agent's favorite mantra
*/
return [location, location, 0];
}
}
/* when an attached object is being moved, detach the objects */
moveWhileAttached(movedObj, newCont)
{
/*
* If I'm the one being moved, detach me from all of my
* non-permanent attachments; otherwise, just detach me from the
* other object, since it's the only one of my attachments being
* moved.
*/
if (movedObj == self)
{
/* I'm being moved - detach from everything */
foreach (local cur in attachedObjects)
{
/*
* If we're not permanently attached to this one, and
* it's not inside me, detach from it. We don't need to
* detach from objects inside this one, because they'll
* be moved along with us automatically.
*/
if (!cur.isIn(self) && !isPermanentlyAttachedTo(cur))
nestedDetachFrom(cur);
}
}
else
{
/* just detach from the one object */
nestedDetachFrom(movedObj);
}
}
/* perform a nested DetachFrom action on the given object */
nestedDetachFrom(obj)
{
/* run the nested DetachFrom as an implied action */
tryImplicitAction(DetachFrom, self, obj);
/*
* if we're still attached to this object, the implied command
* must have failed, so abort the entire action
*/
if (attachedObjects.indexOf(obj) != nil)
exit;
}
;
/*
* Precondition for nearby-attachables. This ensures that the two
* objects being attached are in their negotiated locations.
*/
nearbyAttachableCond: PreCondition
/* carry out the precondition */
checkPreCondition(obj, allowImplicit)
{
local dobjProposal, iobjProposal;
local dobjTargetLoc, iobjTargetLoc;
local iobjRet, dobjRet;
/*
* Ask each of the NearbyAttachable objects (the direct and
* indirect objects) what it thinks. If an object isn't a
* NearbyAttachable, it won't have any opinion, so use a
* placeholder result with an extremely negative priority
* (ensuring that it won't be chosen). In order for this
* precondition to have been triggered, one or the other of the
* objects must have been a nearby-attachable.
*/
dobjProposal = (gDobj.ofKind(NearbyAttachable)
? gDobj.getNearbyAttachmentLocs(gIobj)
: [nil, nil, -2147483648]);
iobjProposal = (gIobj.ofKind(NearbyAttachable)
? gIobj.getNearbyAttachmentLocs(gDobj)
: [nil, nil, -2147483648]);
/*
* If the direct object claims higher priority, use its
* attachment locations; otherwise, use the direct object's
* locations. (This means that we take the indirect object's
* proposed locations if the priorites are equal.)
*/
if (dobjProposal[3] > iobjProposal[3])
{
/* the direct object claims higher priority, so use its results */
dobjTargetLoc = dobjProposal[1];
iobjTargetLoc = dobjProposal[2];
}
else
{
/*
* The direct object doesn't have a higher priority, so use
* the indirect object's results. Note that the iobj
* results list has the iobj in the first position, since it
* was the 'self' when we asked it for its proposal.
*/
dobjTargetLoc = iobjProposal[2];
iobjTargetLoc = iobjProposal[1];
}
/* carry out the pair of moves as needed */
dobjRet = moveObject(gDobj, dobjTargetLoc, allowImplicit);
iobjRet = moveObject(gIobj, iobjTargetLoc, allowImplicit);
/*
* Return the indication of whether or not we carried out an
* implied command. (Note that we can't call moveObject in this
* 'return' expression directly, because of the short-circuit
* behavior of the '||' operator. We must call both, even if
* both carry out an action.)
*/
return (dobjRet || iobjRet);
}
/* carry out an implied action to move an object to a location */
moveObject(obj, loc, allowImplicit)
{
/* if the object is already there, we have nothing to do */
if (obj.location == loc)
return nil;
/* try the implied move */
if (allowImplicit && loc.tryMovingObjInto(obj))
{
/* make sure it worked */
if (obj.location != loc)
exit;
/* we performed an implied action */
return true;
}
/* we can't move it - report failure and abort */
loc.mustMoveObjInto(obj);
exit;
}
;
/*
* A PlugAttachable is a mix-in class that turns PLUG INTO into ATTACH TO
* and UNPLUG FROM into DETACH FROM. This can be combined with
* Attachable or an Attachable subclass for objects that can be attached
* with PLUG INTO commands.
*/
class PlugAttachable: object
/* PLUG IN - to what? */
dobjFor(PlugIn)
{
verify() { }
action() { askForIobj(PlugInto); }
}
/* PLUG INTO is the same as ATTACH TO for us */
dobjFor(PlugInto) remapTo(AttachTo, self, IndirectObject)
iobjFor(PlugInto) remapTo(AttachTo, DirectObject, self)
/* UNPLUG FROM is the same as DETACH FROM */
dobjFor(Unplug) remapTo(Detach, self)
dobjFor(UnplugFrom) remapTo(DetachFrom, self, IndirectObject)
iobjFor(UnplugFrom) remapTo(DetachFrom, DirectObject, self)
;
/* ------------------------------------------------------------------------ */
/*
* Permanent attachments. This class is for things that are described
* in the story text as attached to one another, but which can never be
* separated. This is a mix-in class that can be combined with a Thing
* subclass.
*
* Descriptions of attachment tend to invite the player to try detaching
* the parts; the purpose of this class is to provide responses that are
* better than the defaults. A good custom message for this class
* should usually acknowledge the attachment relationship, and explain
* why the parts can't be separated.
*
* There are two ways to express the attachment relationship.
*
* First, the more flexible way: in each PermanentAttachment object,
* define the 'attachedObjects' property to contain a list of the
* attached objects. All of those other attached objects should usually
* be PermanentAttachment objects themselves, because the real-world
* relationship we're modeling is obviously symmetrical. Because of the
* symmetrical relationship, it's only necessary to include the list
* entry on one side of a pair of attached objects - each side will
* automatically link itself to the other at start-up if it appears in
* the other's attachedObjects list.
*
* Second, the really easy way: if one of the attached objects is
* directly inside the other (which often happens for permanent
* attachments, because one is a component of the other), make the
* parent a PermanentAttachment, make the inner one a
* PermanentAttachmentChild, and you're done. The two will
* automatically link up their attachment lists at start-up.
*
* Note that this is a subclass of Attachable. Note also that a
* PermanentAttachment can be freely combined with a regular Attachable;
* for example, you could create a rope with a hook permanently
* attached, but stil allow the rope to be attached to other things as
* well: you'd make the rope a regular Attachable, and make the hook a
* PermanentAttachment. The hook would be unremovable because of its
* permanent status, and this would symmetrical prevent the rope from
* being removed from the hook. But the rope could still be attached to
* and detached from other objects.
*/
class PermanentAttachment: Attachable
/*
* Get the message explaining why we can't detach from 'obj'.
*
* By default, if our container is also a PermanentAttachment, and
* we're attached to it, we'll simply return its message. This
* makes it really easy to define symmetrical permanent attachment
* relationships using containment, since all you have to do is make
* the container and the child both be PermanentAttachments, and
* then just define the cannot-detach message in the container. If
* the container isn't a PermanentAttachment, or we're not attached
* to it, we'll return our default library message.
*/
cannotDetachMsgFor(obj)
{
if (location != nil
&& location.ofKind(PermanentAttachment)
&& isAttachedTo(location))
return location.cannotDetachMsgFor(obj);
else
return baseCannotDetachMsg;
}
/* basic message to use when we try to detach something from self */
baseCannotDetachMsg = &cannotDetachPermanentMsg
;
/*
* A permanent attachment "child" - this is an attachment that's
* explicitly attached to its container object. This is a convenient
* way of setting up an attachment relationship between container and
* contents when the contents object isn't a Component.
*/
class PermanentAttachmentChild: PermanentAttachment
/* we're attached directly to our container */
attachedObjects = perInstance([location])
;
/* ------------------------------------------------------------------------ */
/*
* A mix-in class for objects that don't come into play until some
* future event. This class lets us initialize these objects with their
* *eventual* location, using the standard '+' syntax, but they won't
* actually appear in the given location until later in the game.
* During pre-initialization, we'll remember the starting location, then
* set the actual location to nil; later, the object can be easily moved
* to its eventual location by calling makePresent().
*/
class PresentLater: object
/*
* My "key" - this is an optional property you can add to a
* PresentLater object to associate it with a group of objects. You
* can then use makePresentByKey() to move every object with a given
* key into the game world at once. This is useful when an event
* triggers a whole set of objects to come into the game world:
* rather than having to write a method that calls makePresent() on
* each of the related objects individually, you can simply give each
* related object the same key value, then call makePresentByKey() on
* that key.
*
* You don't need to define this for an object unless you want to use
* makePresentByKey() with the object.
*/
plKey = nil
/*
* Flag: are we present initially? By default, we're only present
* later, as that's the whole point. In some cases, though, we have
* objects that come and go, but start out present. Setting this
* property to true makes the object present initially, but still
* allows it to come and go using the standard PresentLater
* mechanisms.
*/
initiallyPresent = nil
initializeLocation()
{
/*
* Save the initial location for later, and then clear out the
* current location. We want to start out being out of the game,
* but remember where we'll appear when called upon. To
* accommodate MultiLoc objects, check locationList first.
*/
if (locationList != nil)
{
/* save the location list */
eventualLocation = locationList;
/*
* clear my location list if I'm not initially present; if I
* am initially present, inherit the normal initialization
*/
if (!initiallyPresent)
locationList = [];
else
inherited();
}
else
{
/* save my eventual location */
eventualLocation = location;
/*
* clear my location if I'm not initially present; if I am
* present initially, inherit the normal set-up
*/
if (!initiallyPresent)
location = nil;
else
inherited();
}
}
/* bring the object into the game world in its eventual location(s) */
makePresent()
{
local pc;
/*
* If we have a list, add ourself to each location in the list;
* otherwise, simply move ourself to the single location.
*/
if (eventualLocation != nil && eventualLocation.ofKind(Collection))
eventualLocation.forEach({loc: moveIntoAdd(loc)});
else
moveInto(eventualLocation);
/* if the player character can now see me, mark me as seen */
pc = gPlayerChar;
if (pc.canSee(self))
{
/* mark me as seen */
pc.setHasSeen(self);
/* mark my visible contents as seen */
setContentsSeenBy(pc.visibleInfoTable(), pc);
}
}
/*
* make myself present if the given condition is true; otherwise,
* remove me from the game world (i.e. move me into nil)
*/
makePresentIf(cond)
{
if (cond)
makePresent();
else
moveInto(nil);
}
/*
* Bring every PresentLater object with the given key into the game.
* Note that this is a "class" method that you call on PresentLater
* itself:
*
* PresentLater.makePresentByKey('foo');
*/
makePresentByKey(key)
{
/*
* scan every PresentLater object, and move each one with the
* given key into the game
*/
forEachInstance(PresentLater, function(obj) {
if (obj.plKey == key)
obj.makePresent();
});
}
/*
* Bring every PresentLater object with the given key into the game,
* or move every one out of the game, according to the condition
* 'cond'.
*
* If 'cond' is a function pointer, we'll invoke it once per object
* with the given key, passing the object as the parameter, and use
* the return value as the in game/out of game setting. For example,
* if you wanted to show every object with key 'foo' AND with the
* property 'showObj' set to true, you could write this:
*
* PresentLater.makePresentByKeyIf('foo', {x: x.showObj});
*
* Note that this is a "class" method that you call on PresentLater
* itself.
*/
makePresentByKeyIf(key, cond)
{
/*
* scan every PresentLater object, check each one's key, and make
* each one with the given key present
*/
forEachInstance(PresentLater, function(obj) {
/* consider this object if its key matches */
if (obj.plKey == key)
{
local flag = cond;
/*
* evaluate the condition - if it's a function pointer,
* invoke it on the current object, otherwise just take
* it as a pre-evaluated condition value
*/
if (dataTypeXlat(cond) == TypeFuncPtr)
flag = (cond)(obj);
/* show or hide the object according to the condition */
obj.makePresentIf(flag);
}
});
}
/* our eventual location */
eventualLocation = nil
;
TADS 3 Library Manual
Generated on 5/16/2013 from TADS version 3.1.3