extras.t | documentation |
#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
Generated on 5/16/2013 from TADS version 3.1.3