objects.t | documentation |
#charset "us-ascii" /* * Copyright (c) 2000, 2006 Michael J. Roberts. All Rights Reserved. *. Portions based on work by Kevin Forchione, used by permission. * * TADS 3 Library - objects * * This module defines the basic physical simulation objects (apart from * Thing, the base class for most game objects, which is so large that * it's defined in its own separate module for convenience). We define * such basic classes as containers, surfaces, fixed-in-place objects, * openables, and lockables. */ /* include the library header */ #include "adv3.h" /* ------------------------------------------------------------------------ */ /* * LocateInParent - this is a mix-in superclass that defines the location * of the object as the object's lexical parent. This is useful for * nested object definitions where the next object should be located * within the enclosing object. * * When this class is mixed with Thing or its subclasses, LocateInParent * should go first, so that the location we define here takes precedence. */ class LocateInParent: object location = (lexicalParent) ; /* ------------------------------------------------------------------------ */ /* * Intangible - this is an object that represents something that can be * sensed but which has no tangible existence, such as a ray of light, a * sound, or an odor. */ class Intangible: Thing /* * The base intangible object has no presence in any sense, * including sight. Subclasses should override these as appropriate * for the senses in which the object can be sensed. */ sightPresence = nil soundPresence = nil smellPresence = nil touchPresence = nil /* intangibles aren't included in regular room/inventory/contents lists */ isListed = nil isListedInInventory = nil isListedInContents = nil /* hide intangibles from 'all' for all actions by default */ hideFromAll(action) { return true; } /* don't hide from defaults, though */ hideFromDefault(action) { return nil; } /* * Essentially all verbs are meaningless on intangibles. Each * subclass should re-enable verbs that are meaningful for that * specific type of intangible; to re-enable an action, just define * a verify() handler for the action. * * Note that the verbs we handle via the Default handlers have no * preconditions; since these verbs don't do anything anyway, * there's no need to apply any preconditions to them. */ dobjFor(Default) { preCond = [] verify() { illogical(¬WithIntangibleMsg, self); } } iobjFor(Default) { preCond = [] verify() { illogical(¬WithIntangibleMsg, self); } } ; /* * A "vaporous" object is a visible but intangible object: something * visible, and possibly with an odor and a sound, but not something that * can be touched or otherwise physically manipulated. Fire, smoke, and * fog are examples of this kind of object. */ class Vaporous: Intangible /* we have a sight presence */ sightPresence = true /* * EXAMINE ALL, LISTEN TO ALL, and SMELL ALL apply to us, but hide * from ALL for other actions, as not much else makes sense on us */ hideFromAll(action) { return !(action.ofKind(ExamineAction) || action.ofKind(ListenToAction) || action.ofKind(SmellAction)); } /* * We can examine, smell, and listen to these objects, as normal for * any Thing. To make these verbs work as normal for Thing, we need * to explicitly override the corresponding verifiers, so that we * bypass the dobjFor(Default) verifier in Intangible. We don't need * to do anything special in the overrides, so just inherit the * default handling; what's important is that we do override the * methods at all. */ dobjFor(Examine) { verify() { inherited(); } } dobjFor(Smell) { verify() { inherited(); } } dobjFor(ListenTo) { verify() { inherited(); } } /* * look in, look through, look behind, look under, search: since * vaporous objects are usually essentially transparent, these * commands reveal nothing interesting */ lookInDesc { mainReport(&lookInVaporousMsg, self); } /* * downgrade the likelihood of these slightly, and map everything to * LOOK IN */ dobjFor(LookIn) { verify() { logicalRank(70, 'look in vaporous'); } } dobjFor(LookThrough) asDobjFor(LookIn) dobjFor(LookBehind) asDobjFor(LookIn) dobjFor(LookUnder) asDobjFor(LookIn) dobjFor(Search) asDobjFor(LookIn) /* the message we display for commands we disallow */ notWithIntangibleMsg = ¬WithVaporousMsg ; /* * A sensory emanation. This is an intangible object that represents a * sound, odor, or the like. */ class SensoryEmanation: Intangible /* * Are we currently emanating our sensory information? This can be * used as an on/off switch to control when we're active. */ isEmanating = true /* * The description shown when the *source* is examined (with "listen * to", "smell", or whatever verb is appropriate to the type of sense * the subclass involves). This will also be appended to the regular * "examine" description, if we're not marked as ambient. */ sourceDesc = "" /* our description, with and without being able to see the source */ descWithSource = "" descWithoutSource = "" /* * Our "I am here" message, with and without being able to see the * source. These are displayed in room descriptions, inventory * descriptions, and by the daemon that schedules background messages * for sensory emanations. * * If different messages are desired as the emanation is mentioned * repeatedly while the emanation remains continuously within sense * range of the player character ("A phone is ringing", "The phone is * still ringing", etc), you can do one of two things. The easier * way is to use a Script object; each time we need to show a * message, we'll invoke the script. The other way, which is more * manual but gives you greater control, is to write a method that * checks the displayCount property of self to determine which * iteration of the message is being shown. displayCount is set to 1 * the first time a message is displayed for the object when the * object can first be sensed, and is incremented each we invoke one * of these display routines. Note that displayCount resets to nil * when the object leaves sense scope, so the sequence of messages * will automatically start over each time the object comes back into * scope. * * The manual way (writing a method that checks the displayCount) * might be desirable if you want the emanation to fade into the * background gradually as the player character stays in the same * location repeatedly. This mimics human perception: we notice a * noise or odor most when we first hear it, but if it continues for * an extended period without changing, we'll eventually stop * noticing it. */ hereWithSource = "" hereWithoutSource = "" /* * A message to display when the emanation ceases to be within sense * range. In most cases, this displays nothing at all, but some * emanations might want to note explicitly when the noise/etc * stops. */ noLongerHere = "" /* * Flag: I'm an "ambient" emanation. This means we essentially are * part of the background, and are not worth mentioning in our own * right. If this is set to true, then we won't mention this * emanation at all when it first becomes reachable in its sense. * This should be used for background noises and the like: we won't * ever make an unsolicited mention of them, but they'll still show * up in explicit 'listen' commands and so on. */ isAmbient = nil /* * The schedule for displaying messages about the emanation. This * is a list of intervals between messages, in game clock times. * When the player character can repeatedly sense this emanation for * multiple consecutive turns, we'll use this schedule to display * messages periodically about the noise/odor/etc. * * Human sensory perception tends to be "edge-sensitive," which * means that we tend to perceive sensory input most acutely when * something changes. When a sound or odor is continually present * without variation for an extended period, it tends to fade into * the background of our awareness, so that even though it remains * audible, we gradually stop noticing it. This message display * schedule mechanism is meant to approximate this perceptual model * by allowing the sensory emanation to specify how noticeable the * emanation remains during continuous exposure. Typically, a * continuous emanation would have relatively frequent messages * (every two turns, say) for a couple of iterations, then would * switch to infrequent messages. Emanations that are analogous to * white noise would probably not be mentioned at all after the * first couple of messages, because the human senses are especially * given to treating such input as background. * * We use this list by applying each interval in the list once and * then moving to the next entry in the list. The first entry in * the list is the interval between first sensing the emanation and * displaying the first "still here" message. When we reach the end * of the list, we simply repeat the last interval in the list * indefinitely. If the last entry in the list is nil, though, we * simply never produce another message. */ displaySchedule = [nil] /* * Show our "I am here" description. This is the description shown * as part of our room's description. We show our hereWithSource or * hereWithoutSource message, according to whether or not we can see * the source object. */ emanationHereDesc() { local actor; local prop; /* if we're not currently emanating, there's nothing to do */ if (!isEmanating) return; /* note that we're mentioning the emanation */ noteDisplay(); /* * get the actor driving the description - if there's a command * active, use the command's actor; otherwise use the player * character */ if ((actor = gActor) == nil) actor = gPlayerChar; /* our display varies according to our source's visibility */ prop = (canSeeSource(actor) ? &hereWithSource : &hereWithoutSource); /* * if it's a Script object, invoke the script; otherwise, just * invoke the property */ if (propType(prop) == TypeObject && self.(prop).ofKind(Script)) self.(prop).doScript(); else self.(prop); } /* * Show a message describing that we cannot see the source of this * emanation because the given obstructor is in the way. This * should be overridden for each subclass. */ cannotSeeSource(obs) { } /* * Get the source of the noise/odor/whatever, as perceived by the * current actor. This is the object we appear to be coming from. * By default, an emanation is generated by its direct container, * and by default this is apparent to actors, so we'll simply return * our direct container. * * If the source is not apparent, this should simply return nil. */ getSource() { return location; } /* determine if our source is apparent and visible */ canSeeSource(actor) { local src; /* get our source */ src = getSource(); /* * return true if we have an apparent source, and the apparent * source is visible to the current actor */ return src != nil && actor.canSee(src); } /* * Note that we're displaying a message about the emanation. This * method should be called any time a message about the emanation is * displayed, either by an explicit action or by our background * daemon. * * We'll adjust our next display time so that we wait the full * interval at the current point in the display schedule before we * show any background message about this object. Note we do not * advance through the schedule list; instead, we merely delay any * further message by the interval at the current point in the * schedule list. */ noteDisplay() { /* calculate our next display time */ calcNextDisplayTime(); /* count the display */ if (displayCount == nil) displayCount = 1; else ++displayCount; } /* * Note an indirect message about the emanation. This can be used * when we don't actually display a message ourselves, but another * object (usually our source object) describes the emanation; for * example, if our source object mentions the noise it's making when * it is examined, it should call this method to let us know we have * been described indirectly. This method advances our next display * time, just as noteDisplay() does, but this method doesn't count * the display as a direct display. */ noteIndirectDisplay() { /* calculate our next display time */ calcNextDisplayTime(); } /* * Begin the emanation. This is called from the sense change daemon * when the item first becomes noticeable to the player character - * for example, when the player character first enters the room * containing the emanation, or when the emanation is first * activated. */ startEmanation() { /* if we're an ambient emanation only, don't mention it */ if (isAmbient) return; /* * if we've already initialized our scheduling, we must have * been explicitly mentioned, such as by a room description - in * this case, act as though we're continuing our emanation */ if (scheduleIndex != nil) { continueEmanation(); return; } /* show our message */ emanationHereDesc; } /* * Continue the emanation. This is called on each turn in which the * emanation remains continuously within sense range of the player * character. */ continueEmanation() { /* * if we are not to run again, our next display time will be set * to zero - do nothing in this case */ if (nextDisplayTime == 0 || nextDisplayTime == nil) return; /* if we haven't yet reached our next display time, do nothing */ if (Schedulable.gameClockTime < nextDisplayTime) return; /* * Advance to the next schedule interval, if we have one. If * we're already on the last schedule entry, simply repeat it * forever. */ if (scheduleIndex < displaySchedule.length()) ++scheduleIndex; /* show our description */ emanationHereDesc; } /* * End the emanation. This is called when the player character can * no longer sense the emanation. */ endEmanation() { /* show our "no longer here" message */ noLongerHere; /* uninitialize the display scheduling */ scheduleIndex = nil; nextDisplayTime = nil; /* reset the display count */ displayCount = nil; } /* * Calculate our next display time. The caller must set our * scheduleIndex to the correct index prior to calling this. */ calcNextDisplayTime() { local delta; /* if our scheduling isn't initialized, set it up now */ if (scheduleIndex == nil) { /* start at the first display schedule interval */ scheduleIndex = 1; } /* get the next display interval from the schedule list */ delta = displaySchedule[scheduleIndex]; /* * if the current display interval is nil, it means that we're * never to display another message */ if (delta == nil) { /* * we're not to display again - simply set the next display * time to zero and return */ nextDisplayTime = 0; return; } /* * our next display time is the current game clock time plus the * interval */ nextDisplayTime = Schedulable.gameClockTime + delta; } /* * Internal counters that keep track of our display scheduling. * scheduleIndex is the index in the displaySchedule list of the * interval we're waiting to expire; nextDisplayTime is the game * clock time of our next display. noiseList and odorList are lists * of senseInfo entries for the sound and smell senses, * respectively, indicating which objects were within sense range on * the last turn. displayCount is the number of times in a row * we've displayed a message already. */ scheduleIndex = nil nextDisplayTime = nil noiseList = nil odorList = nil displayCount = nil /* * Class method implementing the sensory change daemon. This runs * on each turn to check for changes in the set of objects the * player can hear and smell, and to generate "still here" messages * for objects continuously within sense range for multiple turns. */ noteSenseChanges() { /* emanations don't change anything, so turn on caching */ libGlobal.enableSenseCache(); /* note sound changes */ noteSenseChangesFor(sound, &noiseList, Noise); /* note odor changes */ noteSenseChangesFor(smell, &odorList, Odor); /* done with sense caching */ libGlobal.disableSenseCache(); } /* * Note sense changes for a particular sense. 'listProp' is the * property of SensoryEmanation giving the list of SenseInfo entries * for the sense on the previous turn. 'sub' is a subclass of ours * (such as Noise) giving the type of sensory emanation used for * this sense. */ noteSenseChangesFor(sense, listProp, sub) { local newInfo; local oldInfo; /* get the old table of SenseInfo entries for the sense */ oldInfo = self.(listProp); /* * Get the new table of items we can reach in the given sense, * and reduce it to include only emanations of the subclass of * interest. */ newInfo = gPlayerChar.senseInfoTable(sense); newInfo.forEachAssoc(function(obj, info) { /* * remove this item if it's not of the subclass of interest, * or if it's not currently emanating */ if (!obj.ofKind(sub) || !obj.isEmanating) newInfo.removeElement(obj); }); /* run through the new list and note each change */ newInfo.forEachAssoc(function(obj, info) { /* treat this as a new command visually */ "<.commandsep>"; /* * Check to see whether the item is starting anew or was * already here on the last turn. If the item was in our * list from the previous turn, it was already here. */ if (oldInfo == nil || oldInfo[obj] == nil) { /* * the item wasn't in sense range on the last turn, so * it is becoming newly noticeable */ obj.startEmanation(); } else { /* the item was already here - continue its emanation */ obj.continueEmanation(); } }); /* run through the old list and note each item no longer sensed */ if (oldInfo != nil) { oldInfo.forEachAssoc(function(obj, info) { /* if this item isn't in the new list, note its departure */ if (newInfo[obj] == nil) { /* treat this as a new command visually */ "<.commandsep>"; /* note the departure */ obj.endEmanation(); } }); } /* store the current list for comparison the next time we run */ self.(listProp) = newInfo; } /* * Examine the sensory emanation. We'll show our descWithSource or * descWithoutSource, according to whether or not we can see the * source object. */ dobjFor(Examine) { verify() { inherited(); } action() { /* note that we're displaying a message about us */ noteDisplay(); /* display our sound description */ if (canSeeSource(gActor)) { /* we can see the source */ descWithSource; } else { local src; /* show the unseen-source version of the description */ descWithoutSource; /* * If we have a source, find out what's keeping us from * seeing the source; in other words, find the opaque * visual obstructor on the sense path to the source. */ if ((src = getSource()) != nil) { local obs; /* get the visual obstructor */ obs = gActor.findVisualObstructor(src); /* * If we found an obstructor, and we can see it, add * a message describing the obstruction. If we * can't see the obstructor, we can't localize the * sensory emanation at all. */ if (obs != nil && gActor.canSee(obs)) cannotSeeSource(obs); } } } } ; /* * Noise - this is an intangible object representing a sound. * * A Noise object is generally placed directly within the object that is * generating the noise. This will ensure that the noise is * automatically in scope whenever the object is in scope (or, more * precisely, whenever the object's contents are in scope) and with the * same sense attributes. * * By default, when a noise is specifically examined via "listen to", * and the container is visible, we'll mention that the noise is coming * from the container. */ class Noise: SensoryEmanation /* * by default, we have a definite presence in the sound sense if * we're emanating our noise */ soundPresence = (isEmanating) /* * By default, a noise is listed in a room description (i.e., on LOOK * or entry to a room) unless it's an ambient background noise.. Set * this to nil to omit the noise from the room description, while * still allowing it to be heard in an explicit LISTEN command. */ isSoundListedInRoom = (!isAmbient && isEmanating) /* show our description as part of a room description */ soundHereDesc() { emanationHereDesc(); } /* explain that we can't see the source because of the obstructor */ cannotSeeSource(obs) { obs.cannotSeeSoundSource(self); } /* treat "listen to" the same as "examine" */ dobjFor(ListenTo) asDobjFor(Examine) /* "examine" requires that the object is audible */ dobjFor(Examine) { preCond = [objAudible] } ; /* * Odor - this is an intangible object representing an odor. */ class Odor: SensoryEmanation /* * by default, we have a definite presence in the smell sense if * we're currently emanating our odor */ smellPresence = (isEmanating) /* * By default, an odor is listed in a room description (i.e., on LOOK * or entry to a room) unless it's an ambient background odor. Set * this to nil to omit the odor from the room description, while * still allowing it to be listed in an explicit SMELL command. */ isSmellListedInRoom = (!isAmbient && isEmanating) /* mention the odor as part of a room description */ smellHereDesc() { emanationHereDesc(); } /* explain that we can't see the source because of the obstructor */ cannotSeeSource(obs) { obs.cannotSeeSmellSource(self); } /* handle "smell" using our "examine" handler */ dobjFor(Smell) asDobjFor(Examine) /* "examine" requires that the object is smellable */ dobjFor(Examine) { preCond = [objSmellable] } ; /* * SimpleNoise is for cases where a noise is an ongoing part of a * location, so (1) it's not necessary to distinguish source and * sourceless versions of the description, and (2) there are no * scheduled reports for the noise. For these cases, all of the * messages default to the basic 'desc' property. Note that we make * this type of noise "ambient" by default, which means that we won't * automatically include it in room descriptions. */ class SimpleNoise: Noise isAmbient = true sourceDesc { desc; } descWithSource { desc; } descWithoutSource { desc; } hereWithSource { desc; } hereWithoutSource { desc; } ; /* SimpleOdor is the olfactory equivalent of SimpleNoise */ class SimpleOdor: Odor isAmbient = true sourceDesc { desc; } descWithSource { desc; } descWithoutSource { desc; } hereWithSource { desc; } hereWithoutSource { desc; } ; /* ------------------------------------------------------------------------ */ /* * Sensory Event. This is an object representing a transient event, * such as a sound, visual display, or odor, to which some objects * observing the event might react. * * A sensory event differs from a sensory emanation in that an emanation * is ongoing and passive, while an event is isolated in time and * actively notifies observers. */ class SensoryEvent: object /* * Trigger the event. This routine must be called at the time when * the event is to occur. We'll notify every interested observer * capable of sensing the event that the event is occurring, so * observers can take appropriate action in response to the event. * * 'source' is the source object - this is the physical object in * the simulation that is causing the event. For example, if the * event is the sound of a phone ringing, the phone would probably * be the source object. The source is used to determine which * observers are capable of detecting the event: an observer must be * able to sense the source object in the appropriate sense to be * notified of the event. */ triggerEvent(source) { /* * Run through all objects connected to the source object by * containment, and notify any that are interested and can * detect the event. Containment is the only way sense * information can propagate, so we can limit our search * accordingly. * * Connection by containment is no guarantee of a sense * connection: it's a necessary, but not sufficient, condition. * Because it's a necessary condition, though, we can use it to * limit the number of objects we have to test with a more * expensive sense path calculation. */ source.connectionTable().forEachAssoc(function(cur, val) { /* * If this object defines the observer notification method, * then it might be interested in the event. If the object * doesn't define this method, then there's no way it could * be interested. (We make this test before checking the * sense path because checking to see if an object defines a * property is fast and simple, while the sense path * calculation could be expensive.) */ if (cur.propDefined(notifyProp, PropDefAny)) { local info; /* * This object might be interested in the event, so * check to see if the object can sense the event. If * this object can sense the source object at all (i.e., * the sense path isn't 'opaque'), then notify the * object of the event. */ info = cur.senseObj(sense, source); if (info.trans != opaque) { /* * this observer object can sense the source of the * event, so notify it of the event */ cur.(notifyProp)(self, source, info); } } }); } /* the sense in which the event is observable */ sense = nil /* * the notification property - this is the property we'll invoke on * each observer to notify it of the event */ notifyProp = nil ; /* * Visual event */ class SightEvent: SensoryEvent sense = sight notifyProp = ¬ifySightEvent ; /* * Visual event observer. This is a mix-in that can be added to any * other classes. */ class SightObserver: object /* * Receive notification of a sight event. This routine is called * whenever a SightEvent occurs within view of this object. * * 'event' is the SightEvent object; 'source' is the physical * simulation object that is making the visual display; and 'info' * is a SenseInfo object describing the viewing conditions from this * object to the source object. */ notifySightEvent(event, source, info) { } ; /* * Sound event */ class SoundEvent: SensoryEvent sense = sound notifyProp = ¬ifySoundEvent ; /* * Sound event observer. This is a mix-in that can be added to any * other classes. */ class SoundObserver: object /* * Receive notification of a sound event. This routine is called * whenever a SoundEvent occurs within hearing range of this object. */ notifySoundEvent(event, source, info) { } ; /* * Smell event */ class SmellEvent: SensoryEvent sense = smell notifyProp = ¬ifySmellEvent ; /* * Smell event observer. This is a mix-in that can be added to any * other classes. */ class SmellObserver: object /* * Receive notification of a smell event. This routine is called * whenever a SmellEvent occurs within smelling range of this * object. */ notifySmellEvent(event, source, info) { } ; /* ------------------------------------------------------------------------ */ /* * Hidden - this is an object that's present but not visible to any * actors. The object will simply not be visible in the 'sight' sense * until discovered. */ class Hidden: Thing /* we can't be seen until discovered */ canBeSensed(sense, trans, ambient) { /* * If the sense is sight, and we haven't been discovered yet, we * cannot be sensed. Otherwise, inherit the normal handling. */ if (sense == sight && !discovered) return nil; else return inherited(sense, trans, ambient); } /* * Have we been discovered yet? * * Note that this should be a simple property value, not a method. * It's risky to make this a method because it's evaluated from * within some of the low-level scope/sense calculations, and those * calculations depend upon certain global variables. If you make * this property into a method, you could indirectly call another * method that changes some of the same globals, which could disrupt * the main scope/sense calculations and cause other, seemingly * unrelated objects to mysteriously appear or disappear at the wrong * times. If you need to calculate this value dynamically, you could * explicitly assign the property a new value in something like a * daemon or an afterAction() method. * * (The warning above is a bit more conservative than is strictly * necessary. It actually is safe to make 'discovered' a method, * *provided* that the method doesn't ever call anything that's * involved in the scope/sense calculations. For example, never call * methods like senseObj(), senseAmbientMax(), or * sensePresenceList(), or anything that calls those. In most cases, * it's safe to call non-sense-related methods, like isOpen() or * isIn().) */ discovered = nil /* mark the object as discovered */ discover() { local pc; /* note that we've been discovered */ discovered = true; /* mark me and my contents as having been seen */ if ((pc = gPlayerChar).canSee(self)) { /* mark me as seen */ pc.setHasSeen(self); /* mark my visible contents as see */ setContentsSeenBy(pc.visibleInfoTable(), pc); } } ; /* ------------------------------------------------------------------------ */ /* * Collective - this is an object that can be used to refer to a group of * other (usually equivalent) objects collectively. In most cases, this * object will be a separate game object that contains or can contain the * individuals: a bag of marbles can be a collective for the marbles, or * a book of matches can be a collective for the matchsticks. * * A collective object is usually given the same plural vocabulary as its * individuals. When we use that plural vocabulary, we will filter for * or against the collective, as determined by the noun phrase * production, when the player uses the collective term. * * This is a mix-in class, intended to be used along with other (usually * Thing-derived) superclasses. */ class Collective: object filterResolveList(lst, action, whichObj, np, requiredNum) { /* scan for my matching individuals */ foreach (local cur in lst) { /* if this one's a matching individual, decide what to do */ if (isCollectiveFor(cur.obj_)) { /* * We're a collective for this object. If the noun * phrase production wants us to filter for collectives, * remove the individual and keep me (the collective); * otherwise, keep the individual and remove me. */ if (np.filterForCollectives) { /* * we want to keep the collective, so remove this * individual item */ lst -= cur; } else { /* * we want to keep individuals, so remove the * collective (i.e., myself) */ lst -= lst.valWhich({x: x.obj_ == self}); /* * we can only be in the list once, so there's no * need to keep looking - if we found another item * for which we're a collective, all we'd do is try * to remove myself again, which would be pointless * since I'm already gone */ break; } } } /* return the result */ return lst; } /* * Determine if I'm a collective object for the given object. * * In order to be a collective for some objects, an object must have * vocubulary for the plural name, and must return true from this * method for the collected objects. */ isCollectiveFor(obj) { return nil; } ; /* * A "collective group" object. This is an abstract object: the player * doesn't think of this as a physically separate object, but rather as a * collection of a bunch of individual objects. For example, if you had * a group of floor-number buttons in an elevator, you might create a * CollectiveGroup to represent the buttons as a collection - from the * player's perspective, there's not a separate physical object called * "the buttons," but it might nonetheless be handy to refer to "the * buttons" collectively as a single entity in commands. CollectiveGroup * is designed for such situations. * * There are two ways to use CollectiveGroup: as a non-physical, * non-simulation object whose only purpose is to field a few specific * commands; or as a physical simulation object that shows up separately * as an object in its own right. * * First: you can use a CollectiveGroup as a non-physical object, which * essentially means it has a nil 'location'. The group object doesn't * actually appear in any location. Instead, it'll be brought into the * sensory system automatically by its individuals, and it'll have the * same effective sensory status as the most visible/audible/etc of its * individuals. This choice is appropriate when the individuals are * mobile, so they might be scattered around the game map, hence the * group object might need to be invoked anywhere. With this option, you * normally won't want to make the CollectiveGroup handle very many * commands, because you'll have to completely customize each command you * want it to handle, in order to properly account for the possible * scattering of the individuals. For example, if you want the group * object to handle the TAKE command, you'll have to figure out which * individuals are in reach, and specially program the procedure for * taking each of the available individuals. * * Second: you can use CollectiveGroup as a simulation object, and * actually set its 'location' to the location of its individuals. The * group object in this case shows up in the simulation alongside its * individuals. This is a good choice if the individuals are fixed in * place, all in one place, because you can simply put the group object * in the same location as the individuals without worrying that the * individuals will move around the game later on. This is much easier * to handle than the first case above, mostly because commands that * physically manipulate the individuals (such as TAKE) aren't a factor. * In this set-up, you can easily let the group object handle many * actions, since it won't have to do much apart from showing the default * failure messages that a Fixed would generate in any other situation. * Note that if you use this approach, the CollectiveGroup should *also* * inherit from Fixture or the like, so that the group object is fixed in * place just like its corresponding individuals. * * The parser will substitute a CollectiveGroup object for its * individuals when (1) any of the individuals are in scope, (2) the * CollectiveGroup has vocabulary that matches a noun phrase in the * player's input, and (3) the conditions for substitution, defined by * isCollectiveQuant and isCollectiveAction, are met. * * (The substitution itself is handled in two steps. First, an * individual will add the group object to the sense connection list * whenever the individual is in the connection list, which will bring * the object into scope, so the parser will be able to match the * vocabulary from the group object any time an individual is in scope. * Once the group object is matched, its filterResolveList method will * throw out either the group object or all of the individuals, depending * on whether or not the isCollectiveQuant and isCollectiveAction tests * are met.) * * For example, we might have a bunch of coins and paper bills in a game, * and give them all a plural word 'money'. We then also create a * collective group object with plural word 'money'. We set the * collectiveGroup property of each coin and bill object to refer to the * collective group object. Whenever the player uses 'money' in a * command, the individual coins and bills will initially match, and the * group object will also match. The group object will then either throw * itself out, keeping only the individuals, or will throw out the * individuals. If the group object decides to field the command, it * will be the only matching object, so a command like "examine money" * will be directed to the single collective group object, rather than * directed to the matching individuals one at a time. This allows the * game to present simpler, more elegant responses to commands on the * individuals as a group. * * By default, the only action we handle is Examine. Each instance must * provide a suitable description so that when the collective is * examined, we describe the group of individuals appropriately. */ class CollectiveGroup: Thing /* collective group objects are usually named in plural terms */ isPlural = true /* * Filter a noun phrase resolution list. * * If there are any objects in the resolution list for which we're a * collective, we'll check to see whether we want to the collective * or keep the individuals. We want to keep the collective if the * action is one we can handle collectively; otherwise, we want to * drop the collective and let the individuals handle the action * instead. * * Note that, when any of our individuals are in scope, we're in * scope. This means that the collective is always in the * resolution list, along with the individuals, if (1) any * individuals are in scope, and (2) the vocabulary used in the noun * phrase matches the collective object. If the vocabulary doesn't * match the collective, the parser simply won't include the * collective in the resolution list by virtue of the normal * vocabulary selection mechanism, so we'll never reach this point. * * By default, the collective object will be ignored if a specific * number of objects is required. When the player explicitly * specifies a quantity (by a phrase like "the five coins" or "both * coins"), we'll assume they want to iterate over individuals * rather than operate on the collection. */ filterResolveList(lst, action, whichObj, np, requiredNum) { /* * If we want to use the collective for the current action and * the required quantity, keep the collective; otherwise, if * there are any individuals, keep the individuals and filter * out the collective group. If there are no matching * individuals, keep the collective group object, since there's * nothing to replace it. */ if (isCollectiveQuant(np, requiredNum) && isCollectiveAction(action, whichObj)) { /* * We can handle the action collectively, so keep myself, and * get rid of the individuals. We want to discard the * individuals because we want the entire action to be * handled by the collective object, rather than iterating * over the individuals. So, discard each object that has * 'self' as a collectiveGroup (which is to say, keep each * object that *doesn't* have collectiveGroup 'self'). */ lst = lst.subset({x: !x.obj_.hasCollectiveGroup(self)}); } else if (lst.indexWhich({x: x.obj_.hasCollectiveGroup(self)}) != nil) { /* * We can't handle the action collectively, and the list * includes at least one of our individuals, so let the * individuals handle it. Simply remove myself from the * list. */ lst = lst.removeElementAt(lst.indexWhich({x: x.obj_ == self})); } /* return the updated list */ return lst; } /* * "Unfilter" a pronoun antecedent list. We'll restore the * individuals to the list so that we can choose anew, for the new * command, whether to select the group object or the individuals. * * For example, suppose there's a CollectiveGroup for a set of * elevator buttons that handles the Examine command, but no other * commands. Now suppose the player types in these commands: * *. >examine buttons *. >push them * * On the first command, the CollectiveGroup object will filter out * the individual buttons in filterResolveList, because the group * object handles the Examine command on behalf of the individuals. * This will set the pronoun antecedent for IT and THEM to the group * object, because that's the program object that handled the * action. On the second command, if the player had typed simply * PUSH BUTTONS, the collective group object would have filtered * *itself* out, keeping the individuals. However, the raw pronoun * binding for THEM is the group object; if we did nothing to change * this, we'd get a different response for PUSH THEM than we'd get * for PUSH BUTTONS. That's where this routine comes in: by * restoring the individuals, we let filterResolveList() make the * decision about what to keep anew for the pronoun. */ expandPronounList(typ, lst) { /* restore our individuals to the list */ forEachInstance(Thing, function(obj) { if (obj.hasCollectiveGroup(self)) lst += obj; }); /* return the list */ return lst; } /* * Check the action to determine if it's one that we want to handle * collectively. If so, return true; if not, return nil. */ isCollectiveAction(action, whichObj) { /* we handle 'Examine' */ if (action.ofKind(ExamineAction)) return true; /* it's not one of ours */ return nil; } /* * Check to see if we're a collective for the given quantity. By * default, we return true only when no quantity is specified. */ isCollectiveQuant(np, requiredNum) { /* if no quantity was specified, use the collective */ return (requiredNum == nil); } /* * Get a list of the individuals that can be sensed, given the * information table for the desired sense (for visible items, this * can be obtained by calling gActor.visibleInfoTable()). This is a * service routine that can be useful for purposes such as writing a * description routine for the collective. For example, a "money" * collective object might want to count up the sum of money visible * and show that. * * Note that it's possible for this to return an empty list. The * caller can deal with this in a description, for example, by * indicating that the collection cannot be seen. */ getVisibleIndividuals(tab) { /* keep only those items that are individuals of this collective */ tab.forEachAssoc(function(key, val) { /* remove this item if it's not an individual of mine */ if (!key.hasCollectiveGroup(self)) tab.removeElement(key); }); /* return a list of the objects (i.e., the table's keys) */ return tab.keysToList(); } /* * When we have no location, we're an abstract object without any * physical presence in the game world. However, we still want to * show up in the senses to the same extent our individuals do. To * do this, we override this method so that we use the same sense * data as the most visible (or whatever) of our individuals. */ addToSenseInfoTable(sense, tab) { /* if we have no location, mimic our best individual */ if (location == nil && !ofKind(BaseMultiLoc)) { /* check everything in the connection table */ tab.forEachAssoc(function(cur, val) { /* if this is one of our individuals, check it */ if (cur.hasCollectiveGroup(self)) { local t; /* * If it's the best or only one so far, adopt its * sense status. Consider it the best if it has a * more transparent transparency than the best so * far, or its transparency is the same and it has a * high ambient level. */ t = transparencyCompare(cur.tmpTrans_, tmpTrans_); if (t > 0 || (t == 0 && cur.tmpAmbient_ > tmpAmbient_)) { /* it's better than our settings; mimic it */ tmpTrans_ = cur.tmpTrans_; tmpAmbient_ = cur.tmpAmbient_; tmpObstructor_ = cur.tmpObstructor_; } } }); } /* inherit the standard handling */ inherited(sense, tab); } /* * When we have no location, we want to create our own special * containment path, just as we create our own special SenseInfo. */ specialPathFrom(src, vec) { /* if we have a location, use the normal handling */ if (location != nil || ofKind(BaseMultiLoc)) inherited(src, vec); /* look for an individual among the source object's connections */ src.connectionTable().forEachAssoc(function(cur, val) { /* if this is one of our individuals, check it */ if (cur.hasCollectiveGroup(self)) { /* add this individual's paths to the vector */ vec.appendAll(src.getAllPathsTo(cur)); } }); } /* * CollectiveGroup objects are not normally listable in any * situations. Since a collective group is merely a parser stand-in * for its individuals, we don't want it to appear as a separate * object in the game. */ isListedInContents = nil isListedInInventory = nil ; /* * An "itemizing" collective group is like a regular collective group, * but the Examine action itemizes the individual visible items making up * the group. We itemize the individuals instead of showing the 'desc' * for the overall group object, as the basic collective group class * does. */ class ItemizingCollectiveGroup: CollectiveGroup /* * Override the main Examine handling. By default, we'll list the * individuals that are visible, and separately list those that are * being carried by the actor. If none of our individuals are * visible, simply say so. */ mainExamine() { local info; local vis; local carried, here; /* get the visible info table */ info = gActor.visibleInfoTable(); /* get the list of visible individuals */ vis = getVisibleIndividuals(info); /* if any individuals are visible, list them */ if (vis.length() != 0) { /* separate out the individuals being carried */ carried = vis.subset({x: x.isIn(gActor)}); here = vis - carried; /* show the items that are here but not being carried, if any */ if (here.length() != 0) { /* get the room contents lister */ local lister = gActor.location.roomContentsLister; /* get the subset that the room contents lister won't list */ local xlist = here.subset({x: !lister.isListed(x)}); /* show the list through the room contents lister */ lister.showList(gActor, nil, here, 0, 0, info, nil); /* Examine any objects not part of the room description */ foreach (local x in xlist) examineUnlisted(x); /* * if that showed anything, add a paragraph break before * the carried list */ if (xlist.length() != 0 && carried.length() != 0) "<.p>"; } /* separately, show the items being carried, if any */ if (carried.length() != 0) gActor.inventoryLister.showList( gActor, gActor, carried, 0, 0, info, nil); } else { /* * None are visible. If it's dark in the location, simply * say so; otherwise, say that we can't see any of me. */ if (!gActor.isLocationLit()) reportFailure(&tooDarkMsg); else reportFailure(&mustBeVisibleMsg, self); } } /* * Examine an unlisted individual object. This will be called for * each object in the room that's not listable via the room contents * lister. */ examineUnlisted(x) { "<.p>"; nestedAction(Examine, x); } ; /* ------------------------------------------------------------------------ */ /* * A readable object. Any ordinary object will show its normal full * description when read, but an object that is explicitly readable will * have elevated logicalness for the "read" action, and can optionally * show a separate description when read. */ class Readable: Thing /* * Show my special reading desription. By default, we set this to * nil to indicate that we should use our default "examine" * description; objects can override this to show a special message * for reading the object as desired. */ readDesc = nil /* our reading description when obscured */ obscuredReadDesc() { gLibMessages.obscuredReadDesc(self); } /* our reading description in dim light */ dimReadDesc() { gLibMessages.dimReadDesc(self); } /* "Read" action */ dobjFor(Read) { verify() { /* give slight preference to an object being held */ if (!isIn(gActor)) logicalRank(80, 'not held'); } action() { /* * if we have a special reading description defined, show * it; otherwise, use the same handling as "examine" */ if (propType(&readDesc) != TypeNil) { local info; /* * Reading requires a transparent sight path and plenty * of light; in the absence of either of these, we can't * make out the details. */ info = gActor.bestVisualInfo(self); if (info.trans != transparent) obscuredReadDesc; else if (info.ambient < 3) dimReadDesc; else readDesc; } else { /* * we have no special reading description, so use the * default "examine" handling */ actionDobjExamine(); } } } ; /* ------------------------------------------------------------------------ */ /* * A "consultable" object. This is an inanimate object that can be * consulted about various topics, almost the way an actor can be asked * about topics. Examples include individual objects that contain * voluminous information, such as books, phone directories, and maps, as * well as collections of individual information-carrying objects, such * as file cabinets or bookcases. * * A consultable keeps a database of TopicEntry objects; this works in * much the same way as the topic database system that actors use. * Create one or more ConsultTopic objects and place them inside the * Consultable (using the 'location' property, or using the '+' syntax). * When an actor consults the object about a topic, we'll search our * database for a ConsultTopic object that matches the topic and is * currently active, and show the response for the best one we can find. * * From an IF design perspective, consultables have two nice properties. * * First, they hide the boundaries of implementation, by letting the game * *suggest* that there's an untold wealth of information in a particular * book (or whatever) without the need to actually implement all of it. * We only have to show the entries the player specifically asks for, so * the game never has to admit when it's run out of things to show, and * the player can never know for sure that there's not more to find. Be * careful, though, because this is a double-edge sword, design-wise; * it's easy to abuse this property to hide information gratuitously from * the player. * * Second, consultables help "match impedances" between the narrative * level of detail and the underlying world model. At the narrative * level, we paint in fairly broad strokes: when we visit a new location, * we describe the *important* features of the setting, not every last * detail. If the player wants to examine something in closer detail, we * zoom in on that detail, assuming we've implemented it, but it's up to * the player to determine where the attention is focused. Consultable * objects give us the same capability for books and the like. With a * consultable, we can describe the way a book looks without immediately * dumping the literal contents of the book onto the screen; but when the * player chooses some aspect of the book to read in detail, we can zoom * in on that page or chapter and show that literal content, if we * choose. * * Also, note that we assume that consultables convey their information * through visual information, such as printed text or a display screen. * Because of this, we by default require that the object be visible to * be consulted. This might not be appropriate in some cases, such as * Braille books or talking PDA's; to remove the visual condition, * override the pre-condition for the Consult action. */ class Consultable: Thing, TopicDatabase /* * If they consult us without a topic, just ask for a topic. Treat * it as logical, but rank it as improbable, in case there's * anything else around that can be consulted without any topic * specified. */ dobjFor(Consult) { preCond = [touchObj, objVisible] verify() { logicalRank(50, 'need a topic'); } action() { askForTopic(ConsultAbout); } } /* consult about a topic */ dobjFor(ConsultAbout) { verify() { } action() { /* remember that we're the last object the actor consulted */ gActor.noteConsultation(self); /* try handling the topic through our topic database */ if (!handleTopic(gActor, gTopic, consultConvType, nil)) topicNotFound(); } } /* show the default response for a topic we couldn't find */ topicNotFound() { /* * Report the absence of the topic. Note that we use an * ordinary, successful report, not a failure report, because * the consultation really did succeed in the sense of the * physical action of consulting: we successfully flipped * through the book, scanned the file cabinet, or whatever. We * didn't find what we were looking for, but in terms of the * physical action undertaken, we successfully did exactly what * we were asked to do. */ mainReport(&cannotFindTopicMsg); } /* * Resolve the topic phrase for a CONSULT ABOUT command. The CONSULT * ABOUT action refers this to the direct object of the action, so * that the direct object can filter the topic match according to * what makes sense for the consultable. * * By default, we resolve the topic phrase a little differently than * we would for conversational commands, such as ASK ABOUT. By * default, we don't differentiate objects at all based on physical * scope or actor knowledge when deciding on a match for a topic * phrase. For example, if you create a Consultable representing a * phone book, and the player enters a command like FIND BOB IN PHONE * BOOK, the topic BOB will be found even if the 'bob' object isn't * known to the player character. The reason for this difference * from ASK ABOUT et al is that consultables are generally the kinds * of objects where, in real life, a person could browse through the * object and come across entries whether or not the person knew * enough to look for them. For example, you could go through a * phone book and find an entry for "Bob" even if you didn't know * anyone named Bob. * * 'lst' is the list of ResolveInfo objects giving the full set of * matches for the vocabulary words; 'np' is the grammar production * object for the topic phrase; and 'resolver' is the TopicResolver * that's resolving the topic phrase. Note that 'lst' contains * ResolveInfo objects, so to get the game-world object for a given * list entry, use lst[i].obj_. * * We return a ResolvedTopic object that encapsulates the matching * objects. * * Note that the resolver object can be used to get certain useful * information. The resolver's getAction() method returns the action * (which you should use instead of gAction, since this routine is * called during the resolution process, not during command * execution); its getTargetActor() method returns the actor * performing the action; and its objInPhysicalScope(obj) method lets * you determine if an object is in physical scope for the actor. */ resolveConsultTopic(lst, np, resolver) { /* * by default, simply return an undifferentiated list with * everything given equal weight, whether known or not, and * whether in scope or not */ return new ResolvedTopic(lst, [], [], np); } /* * Our topic entry database for consultatation topics. This will be * automatically built during initialization from the set of * ConsultTopic objects located within me, so there's usually no * need to initialize this manually. */ consultTopics = nil ; /* * A consultation topic. You can place one or more of these inside a * Consultable object (using the 'location' property, or the '+' * notation), to create a database of topics that can be looked up in * the consultable. */ class ConsultTopic: TopicMatchTopic /* include in the consultation list */ includeInList = [&consultTopics] /* * don't set any pronouns for the topic - the consultable itself * should be the pronoun antecedent */ setTopicPronouns(fromActor, obj) { } ; /* * A default topic entry for a consultable. You can include one (or * more) of these in a consultable's database to provide a topic of last * resort that answers to any topics that aren't in the database * themselves. */ class DefaultConsultTopic: DefaultTopic includeInList = [&consultTopics] setTopicPronouns(fromActor, obj) { } ; /* ------------------------------------------------------------------------ */ /* * A common, abstract base class for things that cannot be moved. You * shouldn't use this class to create game objects directly; you should * always use one of the concrete subclasses, such as Fixture or * Immovable. This base class doesn't provide the full behavior * necessary to make an object immovable; it's just here as a * programming abstraction for the common elements of all immovable * objects. * * This class has two purposes. First, it defines some behavior common * to all non-portable objects. Second, you can test an object to see * if it's based on this class to determine whether it's a portable or * unportable type of Thing. */ class NonPortable: Thing /* * An immovable objects is not listed in room or container contents * listings. Since the object is immovable, it's in effect a * permanent feature of its location, so it should be described as * such: either directly as part of its location's description text, * or via its own specialDesc. */ isListed = nil isListedInContents = nil isListedInInventory = nil /* * By default, if the object's contents would be listed in a direct * examination, then also list them when showing an inventory list, * or describing the enclosing room or an enclosing object. */ contentsListed = (contentsListedInExamine) /* * Are my contents within a fixed item that is within the given * location? Since we're fixed in place, our contents are certainly * within a fixed item, so we merely need to check if we're fixed in * place within the given location. We are if we're in the given * location or we ourselves are fixed in place in the given location. */ contentsInFixedIn(loc) { return isDirectlyIn(loc) || isInFixedIn(loc); } /* * Since non-portables aren't carried, their weight and bulk are * largely irrelevant. Even so, when a non-portable is a component * of another object, or otherwise contained in another object, its * weight and/or bulk can affect the behavior of the parent object. * So, it's simplest to use a default of zero for these so that there * are no surprises about the parent's behavior. */ weight = 0 bulk = 0 /* * Non-portable objects can't be held, since they can't be carried. * However, in some cases, it's useful to include non-portable * objects within an actor, such as when creating component parts of * an actor (hands, say). In these cases, the non-portables aren't * held, but rather are components or similar. */ isHeldBy(actor) { return nil; } /* * We're not being held, but if our location is an actor, then we're * as good as held because we're effectively part of the actor. */ meetsObjHeld(actor) { return actor == location; } /* * showing an immovable to someone simply requires that it be in * sight: we're not holding it up to show it, we're simply pointing * it out */ dobjFor(ShowTo) { preCond = [objVisible] } /* * Thing decreases the likelihood that we want to examine an object * when the object isn't being held. That's fine for portable * objects, but nonportables can never be held, so we don't want that * decrease in logicalness. */ dobjFor(Examine) { /* override Thing's likelihood downgrade for un-held items */ verify() { } } ; /* ------------------------------------------------------------------------ */ /* * A "fixture," which is something that's obviously a part of the room. * These objects cannot be removed from their containers. This class is * meant for permanent features of rooms that obviously cannot be moved * to a new container, such as walls, floors, doors, built-in bookcases, * light switches, buildings, and the like. * * The important feature of a Fixture is that it's *obvious* that it's * part of its container, so it should be safe to assume that a character * normally wouldn't even try to take it or move it. For objects that * might appear portable but turn out to be immovable, other classes are * more appropriate: use Heavy for objects that are immovable simply * because they're very heavy, for example, or Immovable for objects that * are immovable for some non-obvious reason. */ class Fixture: NonPortable /* * Hide fixtures from "all" for certain commands. Fixtures are * obviously part of the location, so a reaonable person wouldn't * even consider trying to do things like take them or move them. */ hideFromAll(action) { return (action.ofKind(TakeAction) || action.ofKind(DropAction) || action.ofKind(PutInAction) || action.ofKind(PutOnAction)); } /* don't hide from defaults, though */ hideFromDefault(action) { return nil; } /* a fixed item can't be moved by an actor action */ verifyMoveTo(newLoc) { /* it's never possible to do this */ illogical(cannotMoveMsg); } /* * a fixed item can't be taken - this would be caught by * verifyMoveTo anyway, but provide a more explicit message when a * fixed item is explicitly taken */ dobjFor(Take) { verify() { illogical(cannotTakeMsg); }} dobjFor(TakeFrom) { verify() { illogical(cannotTakeMsg); }} /* fixed objects can't be put anywhere */ dobjFor(PutIn) { verify() { illogical(cannotPutMsg); }} dobjFor(PutOn) { verify() { illogical(cannotPutMsg); }} dobjFor(PutUnder) { verify() { illogical(cannotPutMsg); }} dobjFor(PutBehind) { verify() { illogical(cannotPutMsg); }} /* fixed objects can't be pushed, pulled, or moved */ dobjFor(Push) { verify() { illogical(cannotMoveMsg); }} dobjFor(Pull) { verify() { illogical(cannotMoveMsg); }} dobjFor(Move) { verify() { illogical(cannotMoveMsg); }} dobjFor(MoveWith) { verify() { illogical(cannotMoveMsg); }} dobjFor(MoveTo) { verify() { illogical(cannotMoveMsg); }} dobjFor(PushTravel) { verify() { illogical(cannotMoveMsg); }} dobjFor(ThrowAt) { verify() { illogical(cannotMoveMsg); }} dobjFor(ThrowDir) { verify() { illogical(cannotMoveMsg); }} /* * The messages to use for illogical messages. These can be * overridden with new properties (of playerActionMessages and the * like), or simply with single-quoted strings to display. */ cannotTakeMsg = &cannotTakeFixtureMsg cannotMoveMsg = &cannotMoveFixtureMsg cannotPutMsg = &cannotPutFixtureMsg /* * A component can be said to be owned by its location's owner or by * its location. */ isOwnedBy(obj) { /* * if I'm owned by the object under the normal rules, then we * won't say otherwise */ if (inherited(obj)) return true; /* * we can be said to be owned by our location, since we're a * direct and permanent part of the location */ if (obj == location) return true; /* * if my location is owned by the given object, consider * ourselves owned by it as well, as we're an extension of our * location */ if (location != nil && location.isOwnedBy(obj)) return true; /* we didn't find anything that establishes ownership */ return nil; } ; /* * A component object. These objects cannot be removed from their * containers because they are permanent features of other objects, which * may themselves be portable: the hands of a watch, a tuning dial on a * radio. This class behaves essentially the same way as Fixture, but * its messages are more suitable for objects that are component parts of * other objects rather than fixed features of rooms. */ class Component: Fixture /* a component cannot be removed from its container by an actor action */ verifyMoveTo(newLoc) { /* it's never possible to do this */ illogical(&cannotMoveComponentMsg, location); } /* * Hide components from EXAMINE ALL, as well as any commands hidden * from ALL for ordinary fixtures. Components are small parts of * larger objects, so when we EXAMINE ALL, it's enough to examine the * larger objects of which we're a part; we don't want components to * show up separately in these cases. */ hideFromAll(action) { /* hide from EXAMINE ALL, plus anything the base class hides */ return (action.ofKind(ExamineAction) || inherited(action)); } /* * We are a component of our direct cotnainer, and we're indirectly a * component of anything that it's a component of. */ isComponentOf(obj) { return (obj == location || (location != nil && location.isComponentOf(obj))); } /* * Consider ourself to be held by the given actor if we're a * component of the actor. */ meetsObjHeld(actor) { return isComponentOf(actor); } /* a component cannot be taken separately */ dobjFor(Take) { verify() { illogical(&cannotTakeComponentMsg, location); }} dobjFor(TakeFrom) { verify() { illogical(&cannotTakeComponentMsg, location); }} /* a component cannot be separately put somewhere */ dobjFor(PutIn) { verify() { illogical(&cannotPutComponentMsg, location); }} dobjFor(PutOn) { verify() { illogical(&cannotPutComponentMsg, location); }} dobjFor(PutUnder) { verify() { illogical(&cannotPutComponentMsg, location); }} dobjFor(PutBehind) { verify() { illogical(&cannotPutComponentMsg, location); }} ; /* * A "secret fixture" is a kind of fixture that we use for internal * implementation purposes, and which we don't intend to be visible to * the player. Objects of this type usually have no vocabulary, since we * don't want the player to be able to refer to them. */ class SecretFixture: Fixture /* * this kind of object is internal to the game's implementation, so * we don't want it to show up in "all" lists */ hideFromAll(action) { return true; } ; /* * A fixture that uses the same custom message for taking, moving, and * putting. In many cases, it's useful to customize the message for a * fixture, using the same custom message for all sorts of moving. Just * override cannotTakeMsg, and the other messages will copy it. */ class CustomFixture: Fixture cannotMoveMsg = (cannotTakeMsg) cannotPutMsg = (cannotTakeMsg) ; /* ------------------------------------------------------------------------ */ /* * An Immovable is an object that can't be moved, but not because it's * obviously a fixture or component of another object. This class is * suitable for things like furniture, which are in principle portable * but which actors aren't actually allowed to pick up or move around. * * Note that Immovable is a lot like Fixture. The difference is that * Fixture is for objects that are *obviously* fixed in place by their * very nature, whereas Immovable is for objects that common sense would * tell us are portable, but which the game doesn't in fact allow the * player to move. * * The practical difference between Immovable and Fixture is that Fixture * considers taking or moving to be illogical actions, whereas Immovable * considers these actions logical but simply doesn't allow them. To be * more specific, Fixture disallows taking and moving in the verify() * methods for those actions, while Immovable disallows the actions in * the check() methods. This means, for example, that Fixture objects * will be removed from consideration during the noun resolution phase * when there are more logical choices. */ class Immovable: NonPortable /* an Immovable can't be taken */ dobjFor(Take) { check() { failCheck(cannotTakeMsg); }} /* Immovables can't be put anywhere */ dobjFor(PutIn) { check() { failCheck(cannotPutMsg); }} dobjFor(PutOn) { check() { failCheck(cannotPutMsg); }} dobjFor(PutUnder) { check() { failCheck(cannotPutMsg); }} dobjFor(PutBehind) { check() { failCheck(cannotPutMsg); }} /* Immovables can't be pushed, pulled, or otherwise moved */ dobjFor(Drop) { action() { reportFailure(cannotMoveMsg); }} dobjFor(Push) { action() { reportFailure(cannotMoveMsg); }} dobjFor(Pull) { action() { reportFailure(cannotMoveMsg); }} dobjFor(Move) { action() { reportFailure(cannotMoveMsg); }} dobjFor(MoveWith) { check() { failCheck(cannotMoveMsg); }} dobjFor(MoveTo) { check() { failCheck(cannotMoveMsg); }} dobjFor(PushTravel) { action() { reportFailure(cannotMoveMsg); }} dobjFor(ThrowAt) { verify() { illogical(cannotMoveMsg); }} dobjFor(ThrowDir) { verify() { illogical(cannotMoveMsg); }} dobjFor(Turn) { verify() { logicalRank(50, 'turn heavy'); } action() { reportFailure(cannotMoveMsg); } } /* * The messages to use for the failure messages. These can be * overridden with new properties (of playerActionMessages and the * like), or simply with single-quoted strings to display. */ cannotTakeMsg = &cannotTakeImmovableMsg cannotMoveMsg = &cannotMoveImmovableMsg cannotPutMsg = &cannotPutImmovableMsg ; /* * An immovable that uses the same custom message for taking, moving, and * putting. In many cases, it's useful to customize the message for an * immovable, using the same custom message for all sorts of moving. * Just override cannotTakeMsg, and the other messages will copy it. */ class CustomImmovable: Immovable cannotMoveMsg = (cannotTakeMsg) cannotPutMsg = (cannotTakeMsg) ; /* * Heavy: an object that's immovable because it's very heavy. This is * suitable for things like large boulders, heavy furniture, or the like: * things that aren't nailed down, but nonetheless are too heavy to be * carried or otherwise move. * * This is a simple specialization of Immovable; the only thing we change * is the messages we use to describe why the object can't be moved. */ class Heavy: Immovable cannotTakeMsg = &cannotTakeHeavyMsg cannotMoveMsg = &cannotMoveHeavyMsg cannotPutMsg = &cannotPutHeavyMsg ; /* ------------------------------------------------------------------------ */ /* * Decoration. This is an object that is included for scenery value but * which has no other purpose, and which the author wants to make clear * is not important. We use the catch-all action routine to respond to * any command on this object with a flat "that's not important" * message, so that the player can plainly see that there's no point * wasting any time trying to manipulate this object. * * We use the "default" catch-all verb verify handling to report our * "that's not important" message, so a decoration can be made * responsive to specific verbs simply by defining an action handler for * those verbs. */ class Decoration: Fixture /* don't include decorations in 'all' */ hideFromAll(action) { return true; } /* don't hide from defaults */ hideFromDefault(action) { return nil; } /* * use the default response "this object isn't important" when we're * used as either a direct or indirect object */ dobjFor(Default) { verify() { illogical(notImportantMsg, self); } } iobjFor(Default) { verify() { illogical(notImportantMsg, self); } } /* use the standard not-important message for decorations */ notImportantMsg = &decorationNotImportantMsg /* * The catch-all Default verifier makes all actions illogical, but we * can override this to allow specific actions by explicitly defining * them here so that they hide the Default verify handlers. In * addition, give decorations a reduced logical rank, so that any * in-scope non-decoration object with similar vocabulary will be * matched for an Examine command ahead of a decoration. */ dobjFor(Examine) { verify() { inherited(); logicalRank(70, 'decoration'); } } /* * likewise for LISTEN TO and SMELL, which are the auditory and * olfactory equivalents of EXAMINE */ dobjFor(ListenTo) { verify() { inherited(); logicalRank(70, 'decoration'); } } dobjFor(Smell) { verify() { inherited(); logicalRank(70, 'decoration'); } } /* likewise for READ */ dobjFor(Read) { verify() { inherited(); logicalRank(70, 'decoration'); } } /* likewise for LOOK IN and SEARCH */ dobjFor(LookIn) { verify() { inherited(); logicalRank(70, 'decoration'); } } dobjFor(Search) { verify() { inherited(); logicalRank(70, 'decoration'); } } /* the default LOOK IN response is our standard "that's not important" */ lookInDesc { mainReport(¬ImportantMsg, self); } ; /* ------------------------------------------------------------------------ */ /* * An "unthing" is an object that represents the *absence* of an object. * It's occasionally useful to respond specially when the player mentions * an object that isn't present, especially when the player is likely to * assume that something is present. * * An unthing is essentially a decoration, but we use a customized * message that says "that isn't here" rather than "that isn't * important". */ class Unthing: Decoration /* * The message to display when the player refers to this object. * This can be a library message property, or a single-quoted string. * This message will probably always be overridden in practice, since * the point of this class is to provide a more specific explanation * of why the object isn't here. */ notHereMsg = &unthingNotHereMsg /* an Unthing shouldn't be picked as a default */ hideFromDefault(action) { return true; } /* * by default, use our 'not here' message for our descriptions (in * all of the standard senses) */ basicExamine() { mainReport(notHereMsg, self); } basicExamineListen(explicit) { if (explicit) mainReport(notHereMsg, self); } basicExamineSmell(explicit) { if (explicit) mainReport(notHereMsg, self); } /* use our custom message for the inherited Decoration responses */ notImportantMsg = (notHereMsg) /* * Because we're not actually here, use custom error messages when * we're used as a possessive or locational qualifier. The standard * messages say things like "Bob doesn't appear to have that" or "You * don't see that in the box," but these don't make sense for an * Unthing - we're not actually here, so we can't "appear" or "seem" * to own or contain anything. Instead, we need to indicate that the * qualifying object itself (i.e., 'self') isn't here at all. */ throwNoMatchForPossessive(txt) { throwUnthingAsQualifier(); } throwNoMatchForLocation(txt) { throwUnthingAsQualifier(); } throwNothingInLocation() { throwUnthingAsQualifier(); } /* * throw a generic message when we're used as a qualifier - we'll * simply get our "not here" message and display that */ throwUnthingAsQualifier() { local msg; /* * resolve our "not here" message to a string - we need to do * this here, since we're too early in the parsing sequence for * the normal "mainResult" type of processing */ msg = MessageResult.resolveMessageText([self], ¬HereMsg, [self]); /* throw a parser exception that will display this literal text */ throw new ParseFailureException(&parserErrorString, msg); } /* * if there's anything at all in a resolve list other than me, always * remove me */ filterResolveList(lst, action, whichObj, np, requiredNum) { /* if the list has anything else in it, remove myself */ if (lst.length() != 1) lst = lst.removeElementAt(lst.indexWhich({x: x.obj_ == self})); /* return the list */ return lst; } /* * trying to given an order to an Unthing acts the same way as any * other kind of interaction */ acceptCommand(issuingActor) { mainReport(notHereMsg, self); } ; /* ------------------------------------------------------------------------ */ /* * Distant item. This is an object that's too far away to manipulate, * but can be seen. This is useful for scenery objects that are at a * great distance within a large location. * * A Distant item is essentially just like a decoration, but the default * message is different. Note that this class is based on Fixture, which * means that it should be *obvious* that the object is too far away to * take or move. */ class Distant: Fixture /* don't include in 'all' */ hideFromAll(action) { return true; } dobjFor(Default) { verify() { illogical(&tooDistantMsg, self); } } iobjFor(Default) { verify() { illogical(&tooDistantMsg, self); } } /* * Explicitly allow examining and listening to a Distant item. To * do this, override the 'verify' methods explicitly; we only need * to inherit the base class handling, but we need to explicitly do * so to 'override' the catch-all default handlers. */ dobjFor(Examine) { verify { inherited() ; } } dobjFor(ListenTo) { verify() { inherited(); } } /* similarly, allow showing a distant item */ dobjFor(ShowTo) { verify() { inherited(); } } ; /* * Out Of Reach - this is a special mix-in that can be used to create an * object that places its *contents* out of reach under customizable * conditions, and can optionally place itself out of reach as well. */ class OutOfReach: object checkTouchViaPath(obj, dest, op) { /* check how we're traversing the object */ if (op == PathTo) { /* * we're reaching from outside for this object itself - * check to see if the source can reach me */ if (!canObjReachSelf(obj)) return new CheckStatusFailure( cannotReachFromOutsideMsg(dest), dest); } else if (op == PathIn) { /* * we're reaching in to touch one of my contents - check to * see if the source object is within reach of my contents */ if (!canObjReachContents(obj)) return new CheckStatusFailure( cannotReachFromOutsideMsg(dest), dest); } else if (op == PathOut) { local ok; /* * We're reaching out. If we're reaching for the object * itself, check to see if we're reachable from within; * otherwise, check to see if we can reach objects outside * us from within. */ if (dest == self) ok = canReachSelfFromInside(obj); else ok = canReachFromInside(obj, dest); /* if we can't reach the object, say so */ if (!ok) return new CheckStatusFailure( cannotReachFromInsideMsg(dest), dest); } /* if we didn't find a problem, allow the operation */ return checkStatusSuccess; } /* * The message to use to indicate that we can't reach an object, * because the actor is outside me and the target is inside, or vice * versa. Each of these can return a property ID giving an actor * action message property, or can simply return a string with the * message text. */ cannotReachFromOutsideMsg(dest) { return &tooDistantMsg; } cannotReachFromInsideMsg(dest) { return &tooDistantMsg; } /* * Determine if the given object can reach my contents. 'obj' is * the object (usually an actor) attempting to reach my contents * from outside of me. * * By default, we'll return nil, so that nothing within me can be * reached from anyone outside. This can be overridden to allow my * contents to become reachable from some external locations but not * others; for example, a high shelf could allow an actor standing * on a chair to reach my contents. */ canObjReachContents(obj) { return nil; } /* * Determine if the given object can reach me. 'obj' is the object * (usually an actor) attempting to reach this object. * * By default, make this object subject to the same rules as its * contents. */ canObjReachSelf(obj) { return canObjReachContents(obj); } /* * Determine if the given object outside of me is reachable from * within me. 'obj' (usually an actor) is attempting to reach * 'dest'. * * By default, we return nil, so nothing outside of me is reachable * from within me. This can be overridden as needed. This should * usually behave symmetrically with canObjReachContents(). */ canReachFromInside(obj, dest) { return nil; } /* * Determine if we can reach this object itself from within. This * is used when 'obj' tries to touch this object when 'obj' is * located within this object. * * By default, we we use the same rules as we use to reach an * external object from within. */ canReachSelfFromInside(obj) { return canReachFromInside(obj, self); } /* * We cannot implicitly remove this obstruction, so simply return * nil when asked. */ tryImplicitRemoveObstructor(sense, obj) { return nil; } ; /* ------------------------------------------------------------------------ */ /* * A Fill Medium - this is the class of object returned from * Thing.fillMedium(). */ class FillMedium: Thing /* * Get the transparency sensing through this medium. */ senseThru(sense) { /* * if I have a meterial, use its transparency; otherwise, we're * transparent */ return (material != nil ? material.senseThru(sense) : transparent); } /* my material */ material = nil ; /* ------------------------------------------------------------------------ */ /* * Base multi-location item with automatic initialization. This is the * base class for various multi-located object classes. * * We provide four ways of initializing a multi-located object's set of * locations. * * First, the object can simply enumerate the locations explicitly, by * setting the 'locationList' property to the list of locations. * * Second, the object can indicate that it's located in every object of a * given class, by setting the 'initialLocationClass' property to the * desired class. * * Third, the object can define a rule that specifies which objects are * its initial locations, by defining the 'isInitiallyIn(obj)' method to * return true if 'obj' is an initial location, nil if not. This can be * combined with the 'initialLocationClass' mechanism: if * 'initialLocationClass' is non-nil, then only objects of the given * class will be tested with 'isInitiallyIn()'; if 'initialLocationClass' * is nil, then every object in the entire game will be tested. * * Fourth, you can override the method buildLocationList() to build an * return the initial list of locations. You can use this approach if * you have a complex set of rules for determining the initial location * list, and none of the above approaches are flexible enough to * implement it. If you override buildLocationList(), simply compute and * return the list of initial locations; the library will automatically * call the method during pre-initialization. * * If you don't define any of these, then the object simply has no * initial locations by default. */ class BaseMultiLoc: object /* * The location list. Instances can override this to manually * enumerate our initial locations. By default, we'll call * buildLocationList() the first time this is invoked, and store the * result. */ locationList = perInstance(buildLocationList()) /* * The class of our initial locations. If this is nil, then our * default buildLocationList() method will test every object in the * entire game with our isInitiallyIn() method; otherwise, we'll test * only objects of the given class. */ initialLocationClass = nil /* * Test an object for inclusion in our initial location list. By * default, we'll simply return true to include every object. We * return true by default so that an instance can merely specify a * value for initialLocationClass in order to place this object in * every instance of the given class. */ isInitiallyIn(obj) { return true; } /* * Build my list of locations, and return the list. This default * implementation looks for an 'initialLocationClass' property value, * and if one is found, looks at every object of that class; * otherwise, it looks at every object in the entire game. In either * case, each object is then passed to our isInitiallyIn() method, * and is included in our result list if isInitiallyIn() returns * true. */ buildLocationList() { /* * If the object doesn't define any of the standard rules, which * it would do by overriding initialLocationClass and/or * isInitiallyIn(), then simply return an empty list. We take * the absence of overrides for any of the rules to mean that the * object simply has no initial locations. */ if (initialLocationClass == nil && !overrides(self, BaseMultiLoc, &isInitiallyIn)) return []; /* start with an empty list */ local lst = new Vector(16); /* * if initialLocationClass is defined, loop over all objects of * that class; otherwise, loop over all objects */ if (initialLocationClass != nil) { /* loop over all instances of the given class */ for (local obj = firstObj(initialLocationClass) ; obj != nil ; obj = nextObj(obj, initialLocationClass)) { /* if the object passes the test, include it */ if (isInitiallyIn(obj)) lst.append(obj); } } else { /* loop over all objects */ for (local obj = firstObj() ; obj != nil ; obj = nextObj(obj)) { /* if the object passes the test, include it */ if (isInitiallyIn(obj)) lst.append(obj); } } /* return the list of locations */ return lst.toList(); } /* determine if I'm in a given object, directly or indirectly */ isIn(obj) { /* first, check to see if I'm directly in the given object */ if (isDirectlyIn(obj)) return true; /* * Look at each object in my location list. For each location * object, if the location is within the object, I'm within the * object. */ return locationList.indexWhich({loc: loc.isIn(obj)}) != nil; } /* determine if I'm directly in the given object */ isDirectlyIn(obj) { /* * we're directly in the given object only if the object is in * my list of immediate locations */ return (locationList.indexOf(obj) != nil); } /* * Determine if I'm to be listed within my immediate container. As a * multi-location object, we have multiple immediate containers, so * we need to know which direct container we're talking about. * Thing.examineListContents() passes this down via "cont:", a named * parameter. Other callers might not always provide this argument, * though, so if it's not present simply base this on whether we have * a special description in any context. */ isListedInContents(examinee:?) { return (examinee != nil ? !useSpecialDescInContents(examinee) : !useSpecialDesc()); } /* Am I either inside 'obj', or equal to 'obj'? */ isOrIsIn(obj) { return self == obj || isIn(obj); } ; /* ------------------------------------------------------------------------ */ /* * MultiLoc: this class can be multiply inherited by any object that * must exist in more than one place at a time. To use this class, put * it BEFORE Thing (or any subclass of Thing) in the object's superclass * list, to ensure that we override the default containment * implementation for the object. * * Note that a MultiLoc object appears *in its entirety* in each of its * locations. This means that MultiLoc is most suitable for a couple of * specific situations: * * - several locations overlap slightly so that they include a common * object: a large statue at the center of a public square, for example; * * - an object forms a sense connection among its location: a window; * * - a distant object that is seen in its entirety from several * locations: the moon, say, or a mountain range. * * Note that MultiLoc is NOT suitable for cases where an object spans * several locations but isn't contained entirely in any one of them: * it's not good for something like a rope or a river, for example. * MultiLoc also isn't good for cases where you simply want to avoid * creating a bunch of repeated decorations in different locations. * MultiLoc isn't good for these cases because a MultiLoc is treated as * though it exists ENTIRELY and SIMULTANEOUSLY in all of its locations, * which means that all of its sense information and internal state is * shared among all of its locations. * * MultiInstance is better than MultiLoc for cases where you want to * share a decoration object across several locations. MultiInstance is * better because it creates individual copies of the object in the * different locations, so each copy has its own separate sense * information and its own separate identity. * * MultiFaceted is better for objects that span several locations, such * as a river or a long rope. Like MultiInstance, MultiFaceted creates * a separate copy in each location; in addition, MultiFaceted relates * the copies together as "facets" of the same object, so that the * parser knows they're all actually parts of one larger object. */ class MultiLoc: BaseMultiLoc /* * Initialize my location's contents list - add myself to my * container during initialization */ initializeLocation() { /* add myself to each of my container's contents lists */ locationList.forEach({loc: loc.addToContents(self)}); } /* * Re-initialize the location list. This calls buildLocationList() * to re-evaluate the location rules, then updates the locationList * to match the new results. We'll remove the MultiLoc from any old * locations that are no longer part of the location list, and we'll * add it to any new locations that weren't previously in the * location list. You can call this at any time to update the * MutliLoc's presence to reflect applying our location rules to the * current game state. * * Note that this doesn't trigger any moveInto notifications. This * routine is a re-initialization rather than an in-game action, so * it's not meant to behave as though an actor in the game were * walking around moving the MultiLoc around; thus no notifications * are sent. Note also that we attempt to minimize our work by * computing the "delta" from the old state - hence we only move the * MultiLoc into containers it wasn't in previously, and we only * remove it from existing containers that it's no longer in. */ reInitializeLocation() { local newList; /* build the new location list */ newList = buildLocationList(); /* * Update any containers that are not in the intersection of the * two lists. Note that we don't simply move ourselves out of * the old list and into the new list, because the two lists * could have common members; to avoid unnecessary work that * might result from removing ourselves from a container and * then adding ourselves right back in to the same container, we * only notify containers when we're actually moving out or * moving in. */ /* * For each item in the old list, if it's not in the new list, * notify the old container that we're being removed. */ foreach (local loc in locationList) { /* if it's not in the new list, remove me from the container */ if (newList.indexOf(loc) == nil) loc.removeFromContents(self); } /* * for each item in the new list, if we weren't already in this * location, add ourselves to the location */ foreach (local loc in newList) { /* if it's not in the old list, add me to the new container */ if (!isDirectlyIn(loc) == nil) loc.addToContents(self); } /* make the new location list current */ locationList = newList; } /* * Note that we don't need to override any of the contents * management methods, since we provide special handling for our * location relationships, not for our contents relationships. */ /* save my location for later restoration */ saveLocation() { /* return my list of locations */ return locationList; } /* restore a previously saved location */ restoreLocation(oldLoc) { /* remove myself from each current location not in the saved list */ foreach (local cur in locationList) { /* * if this present location isn't in the saved list, remove * myself from the location */ if (oldLoc.indexOf(cur) == nil) cur.removeFromContents(self); } /* add myself to each saved location not in the current list */ foreach (local cur in oldLoc) { /* if I'm not already in this location, add me to it */ if (locationList.indexOf(cur) == nil) cur.addToContents(self); } /* set my own list to the original list */ locationList = oldLoc; } /* * Basic routine to move this object into a given single container. * Removes the object from all of its other containers. Performs no * notifications. */ baseMoveInto(newContainer) { /* remove myself from all of my current contents */ locationList.forEach({loc: loc.removeFromContents(self)}); /* set my location list to include only the new location */ if (newContainer != nil) { /* set my new location */ locationList = [newContainer]; /* add myself to my new container's contents */ newContainer.addToContents(self); } else { /* we have no new locations */ locationList = []; } } /* * Add this object to a new location - base version that performs no * notifications. */ baseMoveIntoAdd(newContainer) { /* add the new container to my list of locations */ locationList += newContainer; /* add myself to my new container's contents */ newContainer.addToContents(self); } /* * Add this object to a new location. */ moveIntoAdd(newContainer) { /* notify my new container that I'm about to be added */ if (newContainer != nil) newContainer.sendNotifyInsert(self, newContainer, ¬ifyInsert); /* perform base move-into-add operation */ baseMoveIntoAdd(newContainer); /* note that I've been moved */ moved = true; } /* * Base routine to move myself out of a given container. Performs * no notifications. */ baseMoveOutOf(cont) { /* remove myself from this container's contents list */ cont.removeFromContents(self); /* remove this container from my location list */ locationList -= cont; } /* * Remove myself from a given container, leaving myself in any other * containers. */ moveOutOf(cont) { /* if I'm not actually directly in this container, do nothing */ if (!isDirectlyIn(cont)) return; /* * notify this container (and only this container) that we're * being removed from it */ cont.sendNotifyRemove(obj, nil, ¬ifyRemove); /* perform base operation */ baseMoveOutOf(cont); /* note that I've been moved */ moved = true; } /* * Call a function on each container. We'll invoke the function as * follows for each container 'cont': * * (func)(cont, args...) */ forEachContainer(func, [args]) { /* call the function for each location in our list */ foreach(local cur in locationList) (func)(cur, args...); } /* * Call a function on each connected container. By default, we * don't connect our containers for sense purposes, so we do nothing * here. */ forEachConnectedContainer(func, ...) { } /* * get a list of my connected containers; by default, we don't * connect our containers, so this is an empty list */ getConnectedContainers = [] /* * Clone this object's contents for inclusion in a MultiInstance's * contents tree. A MultiLoc is capable of being in multiple places * at once, so we can just use our original contents tree as is. */ cloneMultiInstanceContents(loc) { } /* * Create a clone of this object for inclusion in a MultiInstance's * contents tree. We don't actually need to make a copy of the * object, because a MultiLoc can be in several locations * simultaneously; all we need to do is add ourselves to the new * location. */ cloneForMultiInstanceContents(loc) { /* add myself into the new container */ baseMoveIntoAdd(loc); } /* * Add the direct containment connections for this item to a lookup * table. * * A MultiLoc does not, by default, connect its multiple locations * together. This means that if we're traversing in from a point of * view outside the MultiLoc object, we don't add any of our other * containers to the connection table. However, the MultiLoc * itself, and its contents, *can* see out to all of its locations; * so if we're traversing from a point of view inside self, we will * add all of our containers to the connection list. */ addDirectConnections(tab) { /* add myself */ tab[self] = true; /* add my CollectiveGroup objects */ foreach (local cur in collectiveGroups) tab[cur] = true; /* add my contents */ foreach (local cur in contents) { if (tab[cur] == nil) cur.addDirectConnections(tab); } /* * If we're traversing from the outside in, don't connect any of * our other containers. However, if we're traversing from our * own point of view, or from a point of view inside us, we do * get to see out to all of our containers. */ if (senseTmp.pointOfView == self || senseTmp.pointOfView.isIn(self)) { /* add my locations */ foreach (local cur in locationList) { if (tab[cur] == nil) cur.addDirectConnections(tab); } } } /* * Transmit ambient energy to my location or locations. Note that * even though we don't by default shine light from one of our * containers to another, we still shine light from within me to * each of our containers. */ shineOnLoc(sense, ambient, fill) { /* shine on each of my containers and their immediate children */ foreach (local cur in locationList) cur.shineFromWithin(self, sense, ambient, fill); } /* * Build a sense path to my location or locations. Note that even * though we don't by default connect our different containers * together, we still build a sense path from within to outside, * because we can see from within out to all of our containers. */ sensePathToLoc(sense, trans, obs, fill) { /* build a path to each of my containers and their children */ foreach (local cur in locationList) cur.sensePathFromWithin(self, sense, trans, obs, fill); } /* * Get the drop destination. The default implementation in Thing * won't work for us, because it delegates to its location to find * the drop destination; we can't do that because we could have * several locations. To figure out which of our multiple locations * to delegate to, we'll look for 'self' in the supplied sense path; * if we can find it, and the previous path element is a container or * peer of ours, then we'll delegate to that container, because it's * the "side" we approached from. If there's no path, or if we're * not preceded in the path by a container of ours, we'll arbitrarily * delegate to our first container. * * Note that when we don't have a path, or there's no container of * ours preceding us in the path, the object being dropped must be * starting inside us. It would be highly unusual for this to happen * with a multi-location object, because MutliLoc isn't designed for * use as a "nested room" or the like. However, it's not an * impossible situation; if the game does want to create such a * scenario, then the game simply needs to override this routine so * that it does whatever makes sense in the game scenario. There's * no general way to handle such situations, but it should be * possible to determine the correct handling for specific scenarios. */ getDropDestination(obj, path) { local idx; /* * if there's no path, get the ordinary "touch" path from the * current actor to us, since this is how the actor would reach * out and touch this object */ if (path == nil) path = gActor.getTouchPathTo(self); /* * if there's a path, check to see if we're in it; if so, and * we're not the first element, and the preceding element is a * container or peer of ours, delegate to the preceding element */ if (path != nil && (idx = path.indexOf(self)) != nil && idx >= 3 && path[idx - 1] is in (PathIn, PathPeer)) { /* * we're preceded in the path by a container or peer of ours, * so we know that we're approaching from that "side" - * delegate to that container, since we're coming from that * direction */ return path[idx - 2].getDropDestination(obj, path); } /* * We either don't have a path, or we're not preceded in the path * by one of our containers or peers, so we don't have any idea * which "side" we're approaching from. This means we have no * good basis for deciding where the object being dropped will * fall. Arbitrarily delegate to our first container, if we have * one. */ return locationList.length() > 0 ? locationList[1].getDropDestination(obj, path) : nil; } ; /* ------------------------------------------------------------------------ */ /* * A "multi-instance" object is a simple way of creating copies of an * object in several places. This is often useful for decorations and * other features that recur in a whole group of rooms. * * You define a multi-instance object in two parts. * * First, you define a MultiInstance object, which is just a hollow * shell of an object that sets up the location relationships. This * shell object doesn't have any presence in the game world; it's just a * programming abstraction. * * Second, as part of the shell object, you define an example of the * object that will actually show up in the game in each of the multiple * locations. You do this by defining a nested object under the * 'instanceObject' property of the shell object. This is otherwise a * perfectly ordinary object. In most cases, you'll want to make this a * Decoration, Fixture, or some other non-portable object class, since * the "cloned" nature of these objects means that you usually won't * want them moving around (if they did, you might run into situations * where you had several of them in the same place, leading to * disambiguation headaches for the player). * * Here's an example of how you set up a multi-instance object: * * trees: MultiInstance *. locationList = [forest1, forest2, forest3] *. instanceObject: Fixture { 'tree/trees' 'trees' *. "Many tall, old trees grow here. " *. isPlural = true *. } *. ; * * Note that the instanceObject itself has no location, because it * doesn't appear in the game-world model itself - it's just a template * for the real objects. * * During initialization, the library will automatically create several * instances (i.e., subclasses) of the example object - one instance per * location, to be exact. These instances are the real objects that * show up in the game world. * * MultiInstance has one more helpful feature: it lets you dynamically * change the set of locations where the instances appear. You do this * using the same interface that you use to move around MultiLoc objects * - moveInto(), moveIntoAdd(), moveOutOf(). When you call these * routines on the MultiInstance shell object, it will add and remove * object instances as needed to keep everything consistent. Thanks to * a little manipulation we do on the instance objects when we set them up, * you can also move the instance objects around directly using * moveInto(), and they'll update the MultiInstance parent to keep its * location list consistent. */ class MultiInstance: BaseMultiLoc /* the template object */ instanceObject = nil /* initialize my locations */ initializeLocation() { /* create a copy of our template object for each of our locations */ locationList.forEach({loc: addInstance(loc)}); } /* * Move the MultiInstance into the given location. This removes us * from any other existing locations and adds us (if we're not * already there) to the given location. */ moveInto(loc) { /* remove all instances that aren't in the new location */ foreach (local cur in instanceList) { /* if this instance isn't directly in 'loc', remove it */ if (!cur.isDirectlyIn(loc)) cur.moveInto(nil); } /* * If I don't have an instance object in the new location, add * one. Since I've dropped every other instance already, we * either have exactly one location now, which is in the new * location, or we have no locations at all; so we need only * check to see if we have any instances and add one in the new * location if not. */ if (loc != nil && locationList.length() == 0) addInstance(loc); } /* * Add the new location to our set of locations. Any existing * locations are unaffected. */ moveIntoAdd(loc) { /* if I'm not already in the location, add an instance there */ if (locationList.indexOf(loc) == nil) addInstance(loc); } /* * Remove me from the given location. Other locations are * unaffected. */ moveOutOf(loc) { local inst; /* find our instance that's in the given location */ inst = getInstanceIn(loc); /* if we found it, remove this instance from its location */ if (inst != nil) inst.moveInto(nil); } /* get our instance object (if any) that's in the given location */ getInstanceIn(loc) { return instanceList.valWhich({x: x.isDirectlyIn(loc)}); } /* internal service routine - add an instance for a given location */ addInstance(loc) { local inst; /* * Create an instance of the template object, mixing in our * special instance superclass using multiple inheritance. The * MultiInstanceInstance superclass overrides the location * manipulation methods so that we keep the MultiInstance parent * (i.e., us) synchronized if we move around the instance object * directly (by calling its moveInto() method directly, for * example). */ inst = TadsObject.createInstanceOf( [instanceMixIn, self], [instanceObject]); /* add it to our list of active instances */ instanceList.append(inst); /* move the instance into its new location */ inst.moveInto(loc); } /* * If any contents are added to the MultiInstance object, they must * be contents of the template object, so add them to the template * object instead of the MultiInstance parent. */ addToContents(obj) { instanceObject.addToContents(obj); } /* * remove an object from our contents - we'll delegate this to our * template object just like we delegate addToContents */ removeFromContents(obj) { instanceObject.removeFromContents(obj); } /* the mix-in superclass for our instance objects */ instanceMixIn = MultiInstanceInstance /* our vector of active instance objects */ instanceList = perInstance(new Vector(5)) ; /* * An instance of a MultiInstance object. This is a mix-in class that * we add (using mutiple inheritance) to each instance. This overrides * the location manipulation methods, to ensure that we keep the * MultiInstance parent object in sync with any changes made directly to * the instance objects. * * IMPORTANT - the library adds this class to each instance object * *automatically*. Game code shouldn't ever have to use this class * directly. */ class MultiInstanceInstance: object construct(parent) { /* remember our MultiInstance parent object */ miParent = parent; /* * clone my contents tree for the new instance, so that we have a * private copy of any components within the instance */ cloneMultiInstanceContents(); } /* move to a new location */ baseMoveInto(newCont) { /* * if we currently have a location, take the location out of our * MultiInstance parent's location list */ if (location != nil) miParent.locationList -= location; /* inherit the standard behavior */ inherited(newCont); /* * if we have a new location, add the new location to our * MultiInstance parent's location list; otherwise, drop out of * the parent's instance list */ if (newCont != nil) { /* * add the new location to the parent's location list, if * we're not already there */ if (miParent.locationList.indexOf(newCont) == nil) miParent.locationList += newCont; } else { /* * we're being removed from the game world, so remove this * instance from the parent's instance list */ miParent.instanceList.removeElement(self); } } /* * All instances of a given MultiInstance are equivalent to one * another, for parsing purposes. */ isEquivalent = true /* our MultiInstance parent */ miParent = nil ; /* ------------------------------------------------------------------------ */ /* * A "multi-faceted" object is similar to a MultiInstance object, with * the addition that the instance objects are "facets" of one another. * This means that they have the same identity, from the perspective of * a character in the scenario: all of the instance objects are part of * the same conceptual object, not separate objects. * * This is especially useful for large objects that span multiple * locations, such as a river or a long rope. * * You define a multi-faceted object the same way you set up a * MultiInstance: definfe a MultiFaceted shell object, and as part of * the shell, define the facet object using the instanceObject property. * Here's an example: * * river: MultiFaceted *. locationList = [riverBank, meadow, canyon] *. instanceObject: Fixture { 'river' 'river' *. "The river meanders by. " *. } *. ; * * The main difference between MultiInstance and MultiFaceted is that * the "facet" objects of a MultiFaceted are related as facets of a * common object from the parser's perspective. For example, if a * player refers to one facet, then travels to another location that * contains a different facet, then refers to "it", the parser will * realize that the pronoun refers to the new facet in the new location. */ class MultiFaceted: MultiInstance /* our instance objects represent our facets for parsing purposes */ getFacets() { return instanceList; } /* the mix-in superclass for our instance objects */ instanceMixIn = MultiFacetedFacet ; /* * The mix-in superclass for MultiFaceted facet instances. * * IMPORTANT - the library adds this class to each instance object * *automatically*. Game code shouldn't ever have to use this class * directly. */ class MultiFacetedFacet: MultiInstanceInstance /* * Get our other facets for parsing purposes - our parent maintains * the list of all of its facets, so simply return that list. (Note * that we'll be in the list as well, but that's harmless, so don't * bother removing us.) */ getFacets() { return miParent.getFacets(); } ; /* ------------------------------------------------------------------------ */ /* * A "linkable" object is one that can participate in a master/slave * relationship. This kind of relationship means that the state of both * objects in the pair is controlled by one of the objects, called the * master; the other object defers to the other to get and set all of * its linkable state. * * Note that this base class doesn't provide for the management of any * of the actual linked state. Subclasses are responsible for doing * this. The general pattern is to create a getter/setter method pair * for each bit of linked state, and in these methods refer to * masterObject.xxx rather than just self.xxx. * * This is useful for objects such as doors that have two separate * objects representing the two sides of the door. The two sides are * always linked for things like open/closed and locked/unlocked state; * this can be handled by linking the two sides, and managing all state * of both sides in one side designated as the master. */ class Linkable: object /* * Get the master object, which holds our state. By default, this * is simply 'self', but some objects might want to override this. * For example, doors are usually implemented with two separate * objects, representing the two sides of the door, which share * common state; in such cases, one of the pair can be designated as * the master, which holds the common state of the door, and this * method can be overridden so that all state operations on the lock * are performed on the master side of the door. * * We return self by default so that a linkable object can stand * alone if desired. That is, a linkable object doesn't have to be * part of a pair; it can just as well be a single object. */ masterObject() { /* * inherit from the next superclass, if possible; otherwise, use * 'self' as the default master object */ if (canInherit()) return inherited(); else return self; } /* * We're normally mixed into a Thing; do some extra work in * initialization. */ initializeThing() { /* inherit the default handling */ inherited(); /* * If we're tied to a separate master object, check the master * object to see if it's tied back to us as its master object. * Only one can be the master; if each says the other is the * master, we'll get stuck in infinite loops as each tries to * defer to the other. To avoid this, break the loop by * arbitrarily choosing one or the other as the master. Note * that we don't have to worry about the other object making a * different decision and breaking the relationship, because if * we detect the loop, it means we're going first - if the other * object had gone first then it would have detected and broken * the loop itself, and we wouldn't be finding the loop now. */ if (masterObject != self && masterObject.masterObject == self) { /* * We're tied together in a loop - break the loop by * arbitrarily electing myself as the master object. * Because these relationships are symmetric, it shouldn't * matter which we choose. */ masterObject = self; } } ; /* ------------------------------------------------------------------------ */ /* * A "basic openable" is an object that keeps open/closed status, and * which can be linked to another object to maintain that status. This * basic class doesn't handle any special commands; it's purely for * keeping track of internal open/closed state. */ class BasicOpenable: Linkable /* * Initial open/closed setting. Set this to true to make the object * open initially. If this object is linked to another object (as * in the two sides of a door), you only need to set this property * in the *master* object - the other side will automatically link * up to the master object during initialization. */ initiallyOpen = nil /* * Flag: door is open. Travel is only possible when the door is * open. Return the master's status. */ isOpen() { /* * If we're the master, simply use our isOpen_ property; * otherwise, call our master's isOpen method. This way, if the * master has a different way of calculating isOpen, we'll defer * to its different handling. */ if (masterObject == self) return isOpen_; else return masterObject.isOpen(); } /* * Make the object open or closed. By default, we'll simply set the * isOpen flag to the new status. Objects can override this to * apply side effects of opening or closing the object. */ makeOpen(stat) { /* * if we're the master, simply set our isOpen_ property; * otherwise, defer to the master */ if (masterObject == self) isOpen_ = stat; else masterObject.makeOpen(stat); /* inherit the next superclass's handling */ inherited(stat); } /* * Open status name. This is an adjective describing whether the * object is opened or closed. In English, this will return "open" * or "closed." */ openDesc = (isOpen ? gLibMessages.openMsg(self) : gLibMessages.closedMsg(self)) /* initialization */ initializeThing() { /* inherit the default handling */ inherited(); /* if we're the master, set our initial open/closed state */ if (masterObject == self) isOpen_ = initiallyOpen; } /* * If we're obstructing a sense path, it must be because we're * closed. Try implicitly opening. */ tryImplicitRemoveObstructor(sense, obj) { /* * If I'm not already open, try opening me. As usual for 'try' * routines, we return true if we attempt a command, nil if not. * * Note that we might be creating an obstruction despite already * being open; in this case, we don't want to do anything, since * an implied 'open' won't help when we're already open. */ return isOpen ? nil : tryImplicitAction(Open, self); } /* * if we can't reach or move something through the container, it * must be because we're closed */ cannotTouchThroughMsg = &cannotTouchThroughClosedMsg cannotMoveThroughMsg = &cannotMoveThroughClosedMsg /* * Internal open/closed status. Do not use this for initialization * - set initiallyOpen in the master object instead. */ isOpen_ = nil ; /* ------------------------------------------------------------------------ */ /* * Openable: a mix-in class that can be combined with an object's other * superclasses to make the object respond to the verbs "open" and * "close." We also add some extra features for other related verbs, * such as a must-be-open precondition "look in" and "board". */ class Openable: BasicOpenable /* * Describe our contents using a special version of the contents * lister, so that we add our open/closed status to the listing. The * message we add is given by our openStatus method, so if all you * want to change is the "it's open" status message, you can just * override openStatus rather than providing a whole new lister. */ descContentsLister = openableDescContentsLister /* * Contents lister to use when we're opening the object. This * lister shows the items that are newly revealed when the object is * opened. */ openingLister = openableOpeningLister /* * Get our "open status" message - this is a complete sentence saying * that we're open or closed. By default, in English, we just say * "it's open" (adjusted for number and gender, of course). * * Note that this message has to be a stand-alone independent clause. * In particular note that we don't put any spacing after it, since * we need to be able to add sentence-ending or clause-ending * punctuation immediately after it. */ openStatus() { return gLibMessages.openStatusMsg(self); } /* * By default, an Openable that's also a Lockable must be closed to * be locked. This means that when it's open, the object is * implicitly unlocked, in which case "It's unlocked" isn't worth * mentioning when the description says "It's open." */ lockStatusReportable = (!isOpen) /* * Action handlers */ dobjFor(Open) { verify() { /* it makes no sense to open something that's already open */ if (isOpen) illogicalAlready(&alreadyOpenMsg); } action() { local trans; /* * note the effect we have currently, while still closed, on * sensing from outside into our contents */ trans = transSensingIn(sight); /* make it open */ makeOpen(true); /* * make the default report - if we make a non-default * report, the default will be ignored, so we don't need to * worry about whether or not we'll make a non-default * report now */ defaultReport(&okayOpenMsg); /* * If the actor is outside me, and we have any listable * contents, and our sight transparency is now better than it * was before we were open, reveal the new contents. * Otherwise, just show our default 'opened' message. * * As a special case, if we're running as an implied command * within a LookIn or Search action on this same object, * don't bother showing this result. Doing so would be * redundant with the explicit examination of the contents * that we'll be doing anyway with the main action. */ if (!gActor.isIn(self) && transparencyCompare(transSensingIn(sight), trans) > 0 && !(gAction.isImplicit && (gAction.parentAction.ofKind(LookInAction) || gAction.parentAction.ofKind(SearchAction)) && gAction.parentAction.getDobj() == self)) { local tab; /* get the table of visible objects */ tab = gActor.visibleInfoTable(); /* show my contents list, if I have any */ openingLister.showList(gActor, self, contents, ListRecurse, 0, tab, nil); /* mark my contents as having been seen */ setContentsSeenBy(tab, gActor); /* show any special contents as well */ examineSpecialContents(); } } } dobjFor(Close) { verify() { /* it makes no sense to close something that's already closed */ if (!isOpen) illogicalAlready(&alreadyClosedMsg); } action() { /* make it closed */ makeOpen(nil); /* show the default report */ defaultReport(&okayCloseMsg); } } dobjFor(LookIn) { /* * to look in an openable object, we must be open, unless the * object is transparent or the actor is inside us */ preCond { local lst; /* get the inherited preconditions */ lst = nilToList(inherited()); /* * if I'm not transparent looking in, and the actor isn't * already inside me, try opening me */ if (transSensingIn(sight) != transparent && !gActor.isIn(self)) lst += objOpen; /* return the result */ return lst; } } dobjFor(Search) { /* * To search an openable object, we must be open - unlike LOOK * IN, this applies even if the object is transparent, since * SEARCH is inherently more aggressive than LOOK IN, and implies * physically picking through the contents. This doesn't apply * if the actor is already inside me. */ preCond { /* get the inherited preconditions */ local lst = nilToList(inherited()); /* if the actor isn't in me, make sure I'm open */ if (!gActor.isIn(self)) lst += objOpen; /* * searching implies physically sifting through the contents, * so we need to be able to touch the object */ lst += touchObj; /* return the updated list */ return lst; } } /* * Generate a precondition to make sure gActor can reach the interior * of the container. We consider the inside reachable if either the * actor is located inside the container, or the actor is outside and * the container is open. */ addInteriorReachableCond(lst) { /* * If the actor's inside us, they can reach our interior whether * we're open or not, so there's no need for any additional * condition. If not, we need to be open for the actor to be * able to reach our interior. */ if (!gActor.isIn(self)) lst = nilToList(lst) + objOpen; /* return the result */ return lst; } iobjFor(PutIn) { /* make sure that our interior is reachable */ preCond { return addInteriorReachableCond(inherited()); } } iobjFor(PourInto) { /* make sure that our interior is reachable */ preCond { return addInteriorReachableCond(inherited()); } } /* can't lock an openable that isn't closed */ dobjFor(Lock) { preCond { return nilToList(inherited()) + objClosed; } } dobjFor(LockWith) { preCond { return nilToList(inherited()) + objClosed; } } /* must be open to get out of a nested room */ dobjFor(GetOutOf) { preCond() { return nilToList(inherited()) + new ObjectPreCondition(self, objOpen); } } /* must be open to get into a nested room */ dobjFor(Board) { preCond() { return nilToList(inherited()) + new ObjectPreCondition(self, objOpen); } } ; /* ------------------------------------------------------------------------ */ /* * Lockable: a mix-in class that can be combined with an object's other * superclasses to make the object respond to the verbs "lock" and * "unlock." A Lockable requires no key. * * Note that Lockable should usually go BEFORE a Thing-derived class in * the superclass list. */ class Lockable: Linkable /* * Our initial locked state (i.e., at the start of the game). By * default, we start out locked. */ initiallyLocked = true /* * Current locked state. Use our isLocked_ status if we're the * master, otherwise defer to the master. */ isLocked() { if (masterObject == self) return isLocked_; else return masterObject.isLocked(); } /* * Make the object locked or unlocked. Objects can override this to * apply side effects of locking or unlocking. By default, if we're * the master, we'll simply set our isLocked_ property to the new * status, and otherwise defer to the master object. */ makeLocked(stat) { /* apply to self or the master object, as appropriate */ if (masterObject == self) isLocked_ = stat; else masterObject.makeLocked(stat); /* inherit the next superclass's handling */ inherited(stat); } /* show our status */ examineStatus() { /* inherit the default handling */ inherited(); /* * if our lock status is visually apparent, and we want to * mention the lock status in our current state, show the lock * status */ if (lockStatusObvious && lockStatusReportable) say(isLocked ? gLibMessages.currentlyLocked : gLibMessages.currentlyUnlocked); } /* * Description of the object's current locked state. In English, * this simply returns one of 'locked' or 'unlocked'. (Note that * this is provided as a convenience to games, for generating * messages about the object that include its state. The library * doesn't use this message itself, so overriding this won't change * any library messages - in particular, it won't change the * examineStatus message.) */ lockedDesc = (isLocked() ? gLibMessages.lockedMsg(self) : gLibMessages.unlockedMsg(self)) /* * Is our 'locked' status obvious? This should be set to true for an * object whose locked/unlocked status can be visually observed, nil * for an object whose status is not visuall apparent. For example, * you can usually tell from the inside that a door is locked by * looking at the position of the lock's paddle, but on the outside * of a door there's usually no way to see the status. * * By default, since we can be locked and unlocked with simple LOCK * and UNLOCK commands, we assume the status is as obvious as the * mechanism must be to allow such simple commands. */ lockStatusObvious = true /* * Is our 'locked' status reportable in our current state? This is * similar to lockStatusObvious, but serves a separate purpose: this * tells us if we wish to report the lock status for aesthetic * reasons. * * This property is primarily of interest to mix-ins. To allow * mix-ins to get a say, regardless of the order of superclasses, * we'll by default defer to any inherited value if there is in fact * an inherited value. If there's no inherited value, we'll simply * return true. * * We use this in the library for one case in particular: when we're * mixed with Openable, we don't want to report the lock status for * an open object because an Openable must by default be closed to be * locked. That is, when an Openable is open, it's always unlocked, * so reporting that it's unlocked is essentially redundant * information. */ lockStatusReportable = (canInherit() ? inherited() : true) /* * Internal locked state. Do not use this to set the initial state * - set initiallyLocked in the master object instead. */ isLocked_ = nil /* initialization */ initializeThing() { /* inherit the default handling */ inherited(); /* if we're the master, set our initial state */ if (masterObject == self) isLocked_ = initiallyLocked; } /* * Action handling */ /* "lock" */ dobjFor(Lock) { preCond = (nilToList(inherited()) + [touchObj]) verify() { /* if we're already locked, there's no point in locking us */ if (isLocked) illogicalAlready(&alreadyLockedMsg); } action() { /* make it locked */ makeLocked(true); /* make the default report */ defaultReport(&okayLockMsg); } } /* "unlock" */ dobjFor(Unlock) { preCond = (nilToList(inherited()) + [touchObj]) verify() { /* if we're already unlocked, there's no point in doing this */ if (!isLocked) illogicalAlready(&alreadyUnlockedMsg); } action() { /* make it unlocked */ makeLocked(nil); /* make the default report */ defaultReport(&okayUnlockMsg); } } /* "lock with" */ dobjFor(LockWith) { preCond = (nilToList(inherited()) + [touchObj]) verify() { illogical(&noKeyNeededMsg); } } /* "unlock with" */ dobjFor(UnlockWith) { preCond = (nilToList(inherited()) + [touchObj]) verify() { illogical(&noKeyNeededMsg); } } /* * Should we automatically unlock this door on OPEN? By default, we * do this only if the lock status is obvious. */ autoUnlockOnOpen = (lockStatusObvious) /* * A locked object can't be opened - apply a precondition and a check * for "open" that ensures that we unlock this object before we can * open it. * * If the lock status isn't obvious, don't try to unlock the object * as a precondition. Instead, test to make sure it's unlocked in * the 'check' routine, and fail. */ dobjFor(Open) { preCond() { /* start with the inherited preconditions */ local ret = nilToList(inherited()); /* automatically unlock on open, if appropriate */ if (autoUnlockOnOpen) ret += objUnlocked; /* return the result */ return ret; } check() { /* make sure we're unlocked */ if (isLocked) { /* let them know we're locked */ reportFailure(&cannotOpenLockedMsg); /* set 'it' to me, so UNLOCK IT works */ gActor.setPronounObj(self); /* we cannot proceed */ exit; } /* inherit the default handling */ inherited(); } } ; /* ------------------------------------------------------------------------ */ /* * A lockable that can't be locked and unlocked by direct action. The * LOCK and UNLOCK commands cannot be used with this kind of lockable. * * This is useful for a couple of situations. First, it's useful when we * want to create a locked object that simply can't be unlocked, such as * a locked door that forms a permanent boundary of the map. Second, * it's useful for locked objects that must be unlocked by some other * means, such as manipulating an external mechanism (pulling a lever, * say). In these cases, the trick is to figure out the separate means * of unlocking the door, so we don't want the LOCK and UNLOCK commands * to work directly. */ class IndirectLockable: Lockable dobjFor(Lock) { check() { reportFailure(cannotLockMsg); exit; } } dobjFor(LockWith) asDobjFor(Lock) dobjFor(Unlock) { check() { reportFailure(cannotUnlockMsg); exit; } } dobjFor(UnlockWith) asDobjFor(Unlock) /* * Since we can't be locked and unlocked with simple LOCK and UNLOCK * commands, presume that the lock status isn't obvious. If the * alternative mechanism that locks and unlocks the object makes the * current status readily apparent, this should be overridden and set * to true. */ lockStatusObvious = nil /* the message we display in response to LOCK/UNLOCK */ cannotLockMsg = &unknownHowToLockMsg cannotUnlockMsg = &unknownHowToUnlockMsg ; /* ------------------------------------------------------------------------ */ /* * LockableWithKey: a mix-in class that can be combined with an object's * other superclasses to make the object respond to the verbs "lock" and * "unlock," with a key as an indirect object. A LockableWithKey cannot * be locked or unlocked except with the keys listed in the keyList * property. * * Note that LockableWithKey should usually go BEFORE a Thing-derived * class in the superclass list. */ class LockableWithKey: Lockable /* * Determine if the key fits this lock. Returns true if so, nil if * not. By default, we'll return true if the key is in my keyList. * This can be overridden to use other key selection criteria. */ keyFitsLock(key) { return keyList.indexOf(key) != nil; } /* * Determine if the key is plausibly of the right type for this * lock. This doesn't check to see if the key actually fits the * lock - rather, this checks to see if the key is generally the * kind of object that might plausibly be used with this lock. * * The point of this routine is to make this class concerned only * with the abstract notion of objects that serve to lock and unlock * other objects, without requiring that the key objects resemble * little notched metal sticks or that the lock objects resemble * cylinders with pins - or, more specifically, without requiring * that all of the kinds of keys in a game remotely resemble one * another. * * For example, one kind of "key" in a game might be a plastic card * with a magnetic stripe, and the corresponding lock would be a * card slot; another kind of key might the traditional notched * metal stick. Clearly, no one would ever think to use a plastic * card with a conventional door lock, nor would one try to put a * house key into a card slot (not with the expectation that it * would actually work, anyway). This routine is meant to * facilitate this kind of distinction: the card slot can use this * routine to indicate that only plastic card objects are plausible * as keys, and door locks can indicate that only metal keys are * plausible. * * This routine can be used for disambiguation and other purposes * when we must programmatically select a key that is not specified * or is only vaguely specified. For example, the keyring searcher * uses it so that, when we're searching for a key on a keyring to * open this lock, we implicitly try only the kinds of keys that * would be plausibly useful for this kind of lock. * * By default, we'll simply return true. Subclasses specific to a * game (such as the "card reader" base class or the "door lock" * base class) can override this to discriminate among the * game-specific key classes. */ keyIsPlausible(key) { return true; } /* the list of objects that can serve as keys for this object */ keyList = [] /* * The list of keys which the player knows will fit this lock. This * is used to make key disambiguation automatic once the player * knows the correct key for a lock. */ knownKeyList = [] /* * Get my known key list. This simply returns the known key list * from the known key owner. */ getKnownKeyList() { return getKnownKeyOwner().knownKeyList; } /* * Get the object that own our known key list. If we explicitly have * our own non-empty known key list, we own the key list; otherwise, * our master object owns the list, as long as it has a non-nil key * list at all. */ getKnownKeyOwner() { /* * if we have a non-empty key list, or our master object doesn't * have a key list at all, use our list; otherwise, use our * master object's list so use our list */ if (knownKeyList.length() != 0 || masterObject.knownKeyList == nil) return self; else return masterObject; } /* * Flag: remember my keys after they're successfully used. If this * is true, whenever a key is successfully used to lock or unlock * this object, we'll add the key to our known key list; * subsequently, whenever we try to use a key in this lock, we will * automatically disambiguate the key based on the keys known to * work previously. * * Some authors might prefer not to assume that the player should * remember which keys operate which locks, so this property can be * changed to nil to eliminate this memory feature. By default we * set this to true, since it shouldn't generally give away any * secrets or puzzles for the game to assume that a key that was * used successfully once with a given lock is the one to be used * subsequently with the same lock. */ rememberKnownKeys = true /* * Determine if the player knows that the given key operates this * lock. Returns true if the key is in our known key list, nil if * not. */ isKeyKnown(key) { return getKnownKeyList().indexOf(key) != nil; } /* * By default, the locked/unlocked status of a keyed lockable is nil. * In most cases, an object that's locked and unlocked using a key * doesn't have a visible indication of the status; for example, you * usually can't tell just by looking at it from the outside whether * or not an exterior door to a building is locked. Usually, the * only way to tell from the outside that an exterior door is locked * is to try opening it and see if it opens. */ lockStatusObvious = nil /* * Should we automatically unlock on OPEN? We will if our inherited * handling says so, OR if the current actor is carrying a key * that's known to work with this object. We automatically unlock * when a known key is present as a convenience: if we have a known * key, then there's no mystery in unlocking this object, and thus * for playability we want to make its operation fully automatic. */ autoUnlockOnOpen() { return (inherited() || getKnownKeyList.indexWhich({x: x.isIn(gActor)}) != nil); } /* * Action handling */ dobjFor(Lock) { preCond { /* * remove any objClosed from our precondition - since we * won't actually do any locking but will instead merely ask * for an indirect object, we don't want to apply the normal * closed precondition here */ return inherited() - objClosed; } verify() { /* if we're already locked, there's no point in locking us */ if (isLocked) illogicalAlready(&alreadyLockedMsg); } action() { /* ask for an indirect object to use as the key */ askForIobj(LockWith); } } /* "unlock" */ dobjFor(Unlock) { verify() { /* if we're not locked, there's no point in unlocking us */ if (!isLocked) illogicalAlready(&alreadyUnlockedMsg); } action() { /* * We need a key. If we're running as an implied action, the * player hasn't specifically proposed unlocking the object, * so it's a little weird to ask a follow-up question about * what key to use. So, if the action is implicit and * there's no default key, don't proceed; simply fail with an * explanation. */ if (gAction.isImplicit && !UnlockWithAction.testRetryDefaultIobj(gAction)) { /* explain that we need a key, and we're done */ reportFailure(&unlockRequiresKeyMsg); return; } /* ask for a key */ askForIobj(UnlockWith); } } /* * perform the action processing for LockWith or UnlockWith - these * are highly symmetrical, in that the only thing that varies is the * new lock state we establish */ lockOrUnlockAction(lock) { /* * If it's a keyring, let the keyring's action handler do the * work. Otherwise, if it's my key, lock/unlock; it's not a * key, fail. */ if (gIobj.ofKind(Keyring)) { /* * do nothing - let the indirect object action handler do * the work */ } else if (keyFitsLock(gIobj)) { local ko; /* * get the object (us or our master object) that owns the * known key list */ ko = getKnownKeyOwner(); /* * if the key owner remembers known keys, and it doesn't know * about this working key yet, remember this in the list of * known keys */ if (ko.rememberKnownKeys && ko.knownKeyList.indexOf(gIobj) == nil) ko.knownKeyList += gIobj; /* set my new state and issue a default report */ makeLocked(lock); defaultReport(lock ? &okayLockMsg : &okayUnlockMsg); } else { /* the key doesn't work in this lock */ reportFailure(&keyDoesNotFitLockMsg); } } /* "lock with" */ dobjFor(LockWith) { verify() { /* if we're already locked, there's no point in locking us */ if (isLocked) illogicalAlready(&alreadyLockedMsg); } action() { /* perform the generic lock/unlock action processing */ lockOrUnlockAction(true); } } /* "unlock with" */ dobjFor(UnlockWith) { verify() { /* if we're not locked, there's no point in unlocking us */ if (!isLocked) illogicalAlready(&alreadyUnlockedMsg); } action() { /* perform the generic lock/unlock action processing */ lockOrUnlockAction(nil); } } ; /* ------------------------------------------------------------------------ */ /* * The common base class for containers and surfaces: things that have * limited bulk capacities. This class isn't usually used directly; * subclasses such as Surface and Container are usually used instead. */ class BulkLimiter: Thing /* * A container can limit the cumulative amount of bulk of its * contents, and the maximum bulk of any one object, using * bulkCapacity and maxSingleBulk. We count the cumulative and * single-item limits separately, since we want to allow modelling * some objects as so large that they won't fit in this container at * all, even if the container is carrying nothing else, without * limiting the number of small items we can carry. * * By default, we set bulkCapacity to a very large number, making * the total capacity of the object essentially unlimited. However, * we set maxSingleBulk to a relatively low number - this way, if an * author wants to designate certain objects as especially large and * thus unable to fit in ordinary containers, the author merely * needs to set the bulk of those large items to something greater * than 10. On the other hand, if an author doesn't want to worry * about bulk and limited carrying capacities and simply uses * library defaults for everything, we will be able to contain * anything and everything. * * In a game that models bulk realistically, a container's bulk * should generally be equal to or slightly greater than its * bulkCapacity, because a container shouldn't be smaller on the * outside than on the inside. If bulkCapacity exceeds bulk, the * player can work around a holding bulk limit by piling objects * into the container, thus "hiding" the bulks of the contents * behind the smaller bulk of the container. */ bulkCapacity = 10000 maxSingleBulk = 10 /* * receive notification that we're about to insert an object into * this container */ notifyInsert(obj, newCont) { /* if I'm the new direct container, check our bulk limit */ if (newCont == self) { /* * do a 'what if' test to see what would happen to our * contained bulk if we moved this item into me */ obj.whatIf({: checkBulkInserted(obj)}, &moveInto, self); } /* inherit base class handling */ inherited(obj, newCont); } /* * Check to see if a proposed insertion - already tentatively in * effect when this routine is called - would overflow our bulk * limits. Reports failure and exits if the inserted object would * exceed our capacity. */ checkBulkInserted(insertedObj) { local objBulk; /* get the bulk of the inserted object itself */ objBulk = insertedObj.getBulk(); /* * Check the object itself to see if it fits by itself. If it * doesn't, we can report the simple fact that the object is too * big for the container. */ if (objBulk > maxSingleBulk || objBulk > bulkCapacity) { reportFailure(&tooLargeForContainerMsg, insertedObj, self); exit; } /* * If our contained bulk is over our maximum, don't allow it. * Note that we merely need to check our current bulk within, * since this routine is called with the insertion already * tentatively in effect. */ if (getBulkWithin() > bulkCapacity) { reportFailure(tooFullMsg, insertedObj, self); exit; } } /* * the message property to use when we're too full to hold a new * object (i.e., the object's bulk would push us over our bulk * capacity limit) */ tooFullMsg = &containerTooFullMsg /* * the message property to use when doing something to one of our * contents would make it too large to fit all by itself into this * container (that is, it would cause that object's bulk to exceed * our maxSingleBulk) */ becomingTooLargeMsg = &becomingTooLargeForContainerMsg /* * the message property to use when doing something to one of our * contents would cause our overall contents to exceed our capacity */ becomingTooFullMsg = &containerBecomingTooFullMsg /* * Check a bulk change of one of my direct contents. */ checkBulkChangeWithin(obj) { local objBulk; /* get the object's new bulk */ objBulk = obj.getBulk(); /* * if this change would cause the object to exceed our * single-item bulk limit, don't allow it */ if (objBulk > maxSingleBulk || objBulk > bulkCapacity) { reportFailure(becomingTooLargeMsg, obj, self); exit; } /* * If our total carrying capacity is exceeded with this change, * don't allow it. Note that 'obj' is already among our * contents when this routine is called, so we can simply check * our current total bulk within. */ if (getBulkWithin() > bulkCapacity) { reportFailure(becomingTooFullMsg, obj, self); exit; } } /* * Adjust a THROW destination. Since we only allow a limited amount * of bulk within our contents, we need to make sure the thrown * object would fit if it landed here. If it doesn't, we'll redirect * the landing site to our container. */ adjustThrowDestination(thrownObj, path) { local thrownBulk = thrownObj.getBulk(); local newBulk; local dest; /* * do a 'what if' test to test our total bulk with the projectile * added to my contents */ newBulk = thrownObj.whatIf({: getBulkWithin()}, &moveInto, self); /* * If that exceeds our maximum bulk, or the object's bulk * individually is over our limit, we can't be the landing site. * In this case, defer to our location's drop destination, if it * has one. */ if ((newBulk > bulkCapacity || thrownBulk > bulkCapacity || thrownBulk > maxSingleBulk) && location != nil && (dest = location.getDropDestination(thrownObj, path)) != nil) { /* * It won't fit, so defer to our container's drop * destination. Give the new destination a chance to further * adjust the destination. */ return dest.adjustThrowDestination(thrownObj, path); } /* * the projectile fits, or we just can't find a container to * defer to; use the original destination, i.e., self */ return self; } /* * Examine my interior. This can be used to handle the action() for * LOOK IN, or for other commands appropriate to the subclass. */ examineInterior() { /* examine the interior with our normal look-in lister */ examineInteriorWithLister(lookInLister); /* * Anything that the an overriding caller (a routine that called * us with 'inherited') wants to add is an addendum to our * description, so add a transcript marker to indicate that the * main description is now finished. * * The important thing about this is that any message that an * overriding caller wants to add is not considered part of the * description, in the sense that we don't want it to suppress * any default description we've already generated. One of the * transformations we apply to the transcript is to suppress any * default descriptive text if there's any more specific * descriptive text following (for example, we suppress "It's an * ordinary <thing>" if we also are going to say "it's open" or * "it contains three coins"). If we have an overriding caller * who's going to add anything, then we must assume that what the * caller's adding is something about the act of examining the * object, rather than a description of the object, so we don't * want it to suppress a default description. */ gTranscript.endDescription(); } /* examine my interior, listing the contents with the given lister */ examineInteriorWithLister(lister) { local tab; /* if desired, reveal any "Hidden" items concealed within */ if (revealHiddenItems) { /* scan our contents and reveal each Hidden item */ foreach (local cur in contents) { /* if it's a Hidden item, reveal it */ if (cur.ofKind(Hidden)) cur.discover(); } } /* get my visible sense info */ tab = gActor.visibleInfoTable(); /* show my contents, if I have any */ lister.showList(gActor, self, contents, ListRecurse, 0, tab, nil); /* mark my contents as having been seen */ setContentsSeenBy(tab, gActor); /* examine my special contents */ examineSpecialContents(); } /* * Verify putting something new in my interior. This is suitable * for use as a verify() method for a command like PutIn or PutOn. * Note that this routine assumes and requires that gDobj be the * object to be added, and gIobj be self. */ verifyPutInInterior() { /* * if we haven't resolved the direct object yet, we can at least * check to see if all of the potential direct objects are * already in me, and rule out this indirect object as illogical * if so */ if (gDobj == nil) { /* * check the tentative direct objects to see if (1) all of * them are directly inside me already, or (2) all of them * are at least indirectly inside me already */ if (gTentativeDobj.indexWhich( {x: !x.obj_.isDirectlyIn(self)}) == nil) { /* * All of the potential direct objects are already * directly inside me. This makes this object * illogical, since there's no need to move any of these * objects into me. */ illogicalAlready(&alreadyPutInMsg); } else if (gTentativeDobj.indexWhich( {x: !x.obj_.isIn(self)}) == nil) { /* * All of the potential direct objects are already in * me, at least indirectly. This makes this object * somewhat less likely, since we're more likely to want * to put something in here that wasn't already within. * Note that this isn't actually illogical, though, * since we could be moving something from deeper inside * me to directly inside me. */ logicalRank(50, 'dobjs already inside'); } } else { /* * We can't put myself in myself, obviously. We also can't * put something into any component of itself, so the command * is illogical if we're a component of the direct object. */ if (gDobj == self || isComponentOf(gDobj)) illogicalSelf(&cannotPutInSelfMsg); /* if it's already directly inside me, this is illogical */ if (gDobj.isDirectlyIn(self)) illogicalAlready(&alreadyPutInMsg); } /* * if I'm not held by the actor, give myself a slightly lower * ranking than fully logical, so that objects being held are * preferred */ if (!isIn(gActor)) logicalRank(60, 'not indirectly held'); else if (!isHeldBy(gActor)) logicalRank(70, 'not held'); } /* * Flag: reveal any hidden items contained directly within me when * my interior is explicitly examined, via a command such as LOOK IN * <self>. By default, we reveal our hidden contents on * examination; hidden objects are in most cases meant to be more * inconspicuous than actually camouflaged, so a careful, explicit * examination would normally reveal them. If our hidden objects * are so concealed that even explicit examination of our interior * wouldn't reveal them, set this to nil. */ revealHiddenItems = true ; /* ------------------------------------------------------------------------ */ /* * A basic container is an object that can enclose its contents. This is * the core of the Container type, but this class only has the bare-bones * sense-related enclosing features, without any action implementation. * This can be used for cases where an object isn't meant to have its * contents be manipulable by the player (so we don't want to allow "put * in" and so on), but where we do want the ability to conceal our * contents when we're closed. */ class BasicContainer: BulkLimiter /* * My current open/closed state. By default, this state never * changes, but is fixed in the object's definition; for example, a * box without a lid would always be open, while a hollow glass cube * would always be closed. Our default state is open. */ isOpen = true /* the material that we're made of */ material = adventium /* prepositional phrase for objects being put into me */ putDestMessage = &putDestContainer /* * Determine if I can move an object via a path through this * container. */ checkMoveViaPath(obj, dest, op) { /* * if we're moving the object in or out of me, we must consider * our openness and whether or not the object fits through our * opening */ if (op is in (PathIn, PathOut)) { /* if we're closed, we can't move anything in or out */ if (!isOpen) return new CheckStatusFailure(cannotMoveThroughMsg, obj, self); /* if it doesn't fit through our opening, don't allow it */ if (!canFitObjThruOpening(obj)) return new CheckStatusFailure(op == PathIn ? &cannotFitIntoOpeningMsg : &cannotFitOutOfOpeningMsg, obj, self); } /* in any other cases, allow the operation */ return checkStatusSuccess; } /* * The message property we use when we can't move an object through * the containment boundary. This is a playerActionMessages * property. */ cannotMoveThroughMsg = &cannotMoveThroughContainerMsg /* * Determine if an actor can touch an object via a path through this * container. */ checkTouchViaPath(obj, dest, op) { /* * if we're reaching from inside directly to me, allow it - * treat this as touching our interior, which we allow from * within regardless of our open/closed status */ if (op == PathOut && dest == self) return checkStatusSuccess; /* * if we're reaching in or out of me, consider our openness and * whether or not the actor's hand fits through our opening */ if (op is in (PathIn, PathOut)) { /* if we're closed, we can't reach into/out of the container */ if (!isOpen) return new CheckStatusFailure(cannotTouchThroughMsg, obj, self); /* * if the object's "hand" doesn't fit through our opening, * don't allow it */ if (!canObjReachThruOpening(obj)) return new CheckStatusFailure(op == PathIn ? &cannotReachIntoOpeningMsg : &cannotReachOutOfOpeningMsg, obj, self); } /* in any other cases, allow the operation */ return checkStatusSuccess; } /* * Library message (in playerActionMessages) explaining why we can't * touch an object through this container. This is used when an * actor on the outside tries to reach something on the inside, or * vice versa. */ cannotTouchThroughMsg = &cannotTouchThroughContainerMsg /* * Determine if the given object fits through our opening. This is * only called when we're open; this determines if the object can be * moved in or out of this container. By default, we'll return * true; some objects might want to override this to disallow * objects over a certain size from being moved in or out of this * container. * * Note that this method doesn't care whether or not the object can * actually fit inside the container once through the opening; we * only care about whether or not the object can fit through the * opening itself. This allows for things like narrow-mouthed * bottles which have greater capacity within than in their * openings. */ canFitObjThruOpening(obj) { return true; } /* * Determine if the given object can "reach" through our opening, * for the purposes of touching an object on the other side of the * opening. This is used to determine if the object, which is * usually an actor, can its "hand" (or whatever appendange 'obj' * uses to reach things) through our opening. This is only called * when we're open. By default, we'll simply return true. * * This differs from canFitObjThruOpening() in that we don't care if * all of 'obj' is able to fit through the opening; we only care * whether obj's hand (or whatever it uses for reaching) can fit. */ canObjReachThruOpening(obj) { return true; } /* * Determine how a sense passes to my contents. If I'm open, the * sense passes through directly, since there's nothing in the way. * If I'm closed, the sense must pass through my material. */ transSensingIn(sense) { if (isOpen) { /* I'm open, so the sense passes through without interference */ return transparent; } else { /* I'm closed, so the sense must pass through my material */ return material.senseThru(sense); } } /* * Get my fill medium. If I'm open, inherit my parent's medium, * assuming that the medium behaves like fog or smoke and naturally * disperses to fill any nested open containers. If I'm closed, I * am by default filled with no medium. */ fillMedium() { if (isOpen && location != nil) { /* I'm open, so return my location's medium */ return location.fillMedium(); } else { /* * I'm closed, so we're cut off from the parent - assume * we're filled with nothing */ return nil; } } /* * Display a message explaining why we are obstructing a sense path * to the given object. */ cannotReachObject(obj) { /* * We must be obstructing by containment. Show an appropriate * message depending on whether the object is inside me or not - * if not, then the actor trying to reach the object must be * inside me. */ if (obj.isIn(self)) gLibMessages.cannotReachContents(obj, self); else gLibMessages.cannotReachOutside(obj, self); } /* explain why we can't see the source of a sound */ cannotSeeSoundSource(obj) { /* we must be obstructing by containment */ if (obj.isIn(self)) gLibMessages.soundIsFromWithin(obj, self); else gLibMessages.soundIsFromWithout(obj, self); } /* explain why we can't see the source of an odor */ cannotSeeSmellSource(obj) { /* we must be obstructing by containment */ if (obj.isIn(self)) gLibMessages.smellIsFromWithin(obj, self); else gLibMessages.smellIsFromWithout(obj, self); } /* message when an object is too large (all by itself) to fit in me */ tooLargeForContainerMsg = &tooLargeForContainerMsg ; /* ------------------------------------------------------------------------ */ /* * Container: an object that can have other objects placed within it. */ class Container: BasicContainer /* * Our fixed "look in" description, if any. This is shown on LOOK * IN before our normal listing of our portable contents; it can be * used to describe generally what the interior looks like, for * example. By default, we show nothing here. */ lookInDesc = nil /* * Show our status for "examine". This shows our open/closed status, * and lists our contents. */ examineStatus() { /* show any special container-specific status */ examineContainerStatus(); /* inherit the default handling to show my contents */ inherited(); } /* * mention my open/closed status for Examine processing */ examineContainerStatus() { /* * By default, show nothing extra. This can be overridden by * subclasses as needed to show any extra status before our * contents list. */ } /* * Try putting an object into me when I'm serving as a bag of * holding. For a container, this simply does a "put obj in bag". */ tryPuttingObjInBag(target) { /* if the object won't fit all by itself, don't even try */ if (target.getBulk() > maxSingleBulk) return nil; /* if we can't fit the object with other contents, don't try */ if (target.whatIf({: getBulkWithin() > bulkCapacity}, &moveInto, self)) return nil; /* we're a container, so use "put in" to get the object */ return tryImplicitActionMsg(&announceMoveToBag, PutIn, target, self); } /* * Try moving an object into this container. For a container, this * performs a PUT IN command to move the object into self. */ tryMovingObjInto(obj) { return tryImplicitAction(PutIn, obj, self); } /* -------------------------------------------------------------------- */ /* * "Look in" */ dobjFor(LookIn) { verify() { } check() { /* * If I'm closed, and I can't see my contents when closed, we * can't go on. Unless, of course, the actor is inside us, * in which case our external boundary isn't relevant. */ if (!isOpen && transSensingIn(sight) == opaque && !gActor.isIn(self)) { /* we can't see anything because we're closed */ reportFailure(&cannotLookInClosedMsg); exit; } } action() { /* show our fixed "look in" description, if any */ lookInDesc; /* examine my interior */ examineInterior(); } } /* * "Search". This is mostly like Open, except that the actor has to * be able to reach into the object, not just see into it - searching * implies a more thorough sort of examination, usually including * physically poking through the object's contents. */ dobjFor(Search) { preCond = (nilToList(inherited()) + [touchObj]) check() { /* * if I'm closed, and the actor isn't inside me, make sure my * contents are reachable from the outside */ if (!isOpen && transSensingIn(touch) != transparent && !gActor.isIn(self)) { /* we can't search an object that we can't reach into */ reportFailure(&cannotTouchThroughMsg, gActor, self); exit; } } } /* -------------------------------------------------------------------- */ /* * Put In processing. A container can accept new contents. */ iobjFor(PutIn) { verify() { /* use the standard verification for adding new contents */ verifyPutInInterior(); } action() { /* move the direct object into me */ gDobj.moveInto(self); /* issue our default acknowledgment of the command */ defaultReport(&okayPutInMsg); } } ; /* * A "restricted holder" is a generic mix-in class for various container * types (Containers, Surfaces, Undersides, RearContainers, RearSurfaces) * that adds a restriction to what can be contained. */ class RestrictedHolder: object /* * A list of acceptable items for the container. This list can be * used to identify the objects that can be put in the container (or * on the surface, under the underside, or behind the rear container * or surface). */ validContents = [] /* * Is the given object allowed to go in this container (or * on/under/behind it, as appropriate for the type)? Returns true if * so, nil if not. By default, we'll return true if the object is * found in our validContents list, nil if not. This can be * overridden if a subclass wants to determine which objects are * acceptable with some other kind of per-object test; for example, a * subclass might accept only objects of a given class as contents, * or might accept only contents with some particular attribute. */ canPutIn(obj) { return validContents.indexOf(obj) != nil; } /* * Check a PUT IN/ON/UNDER/BEHIND action to ensure that the direct * object is in our approved-contents list. */ checkPutDobj(msgProp) { /* validate the direct object */ if (!canPutIn(gDobj)) { /* explain the problem */ reportFailure(self.(msgProp)(gDobj)); /* terminate the command */ exit; } } ; /* * A special kind of container that only accepts specific contents. The * acceptable contents can be specified by a list of enumerated items, * or by a method that indicates whether or not an item is allowed. */ class RestrictedContainer: RestrictedHolder, Container /* * A message that explains why the direct object can't be put in this * container. In most cases, the rather generic default message * should be overridden to provide a specific reason that the dobj * can't be put in this object. 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. */ cannotPutInMsg(obj) { return &cannotPutInRestrictedMsg; } /* override PutIn to enforce our contents restriction */ iobjFor(PutIn) { check() { checkPutDobj(&cannotPutInMsg); } } ; /* * A single container is a special kind of container that can only * contain a single item. If another object is put into this container, * we'll remove any current contents. */ class SingleContainer: Container /* override PutIn to enforce our single-contents rule */ iobjFor(PutIn) { preCond { return inherited() + objEmpty; } } ; /* ------------------------------------------------------------------------ */ /* * OpenableContainer: an object that can contain things, and which can * be opened and closed. */ class OpenableContainer: Openable, Container ; /* ------------------------------------------------------------------------ */ /* * LockableContainer: an object that can contain things, and that can be * opened and closed as well as locked and unlocked. */ class LockableContainer: Lockable, OpenableContainer ; /* ------------------------------------------------------------------------ */ /* * KeyedContainer: an openable container that can be locked and * unlocked, but only with a specified key. */ class KeyedContainer: LockableWithKey, OpenableContainer ; /* ------------------------------------------------------------------------ */ /* * Surface: an object that can have other objects placed on top of it. * A surface is essentially the same as a regular container, but the * contents of a surface behave as though they are on the surface's top * rather than contained within the object. */ class Surface: BulkLimiter /* * Our fixed LOOK IN description. This is shown in response to LOOK * IN before we list our portable contents; it can be used to show * generally what the surface looks like. By default, we say * nothing here. */ lookInDesc = nil /* my contents lister */ contentsLister = surfaceContentsLister descContentsLister = surfaceDescContentsLister lookInLister = surfaceLookInLister inlineContentsLister = surfaceInlineContentsLister /* * we're a surface, so taking something from me that's not among my * contents shows the message as "that's not on the iobj" */ takeFromNotInMessage = &takeFromNotOnMsg /* * my message indicating that another object x cannot be put into me * because I'm already in x */ circularlyInMessage = &circularlyOnMsg /* message phrase for objects put into me */ putDestMessage = &putDestSurface /* message when we're too full for another object */ tooFullMsg = &surfaceTooFullMsg /* * Try moving an object into this container. For a surface, this * performs a PUT ON command to move the object onto self. */ tryMovingObjInto(obj) { return tryImplicitAction(PutOn, obj, self); } /* -------------------------------------------------------------------- */ /* * Put On processing */ iobjFor(PutOn) { verify() { /* use the standard put-in verification */ verifyPutInInterior(); } action() { /* move the direct object onto me */ gDobj.moveInto(self); /* issue our default acknowledgment */ defaultReport(&okayPutOnMsg); } } /* * Looking "in" a surface simply shows the surface's contents. */ dobjFor(LookIn) { verify() { } action() { /* show our fixed lookInDesc */ lookInDesc; /* show our contents */ examineInterior(); } } /* use the PUT ON forms of the verifier messages */ cannotPutInSelfMsg = &cannotPutOnSelfMsg alreadyPutInMsg = &alreadyPutOnMsg ; /* * A special kind of surface that only accepts specific contents. */ class RestrictedSurface: RestrictedHolder, Surface /* * A message that explains why the direct object can't be put on this * surface. In most cases, the rather generic default message should * be overridden to provide a specific reason that the dobj can't be * put on this surface. 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. */ cannotPutOnMsg(obj) { return &cannotPutOnRestrictedMsg; } /* override PutOn to enforce our contents restriction */ iobjFor(PutOn) { check() { checkPutDobj(&cannotPutOnMsg); } } ; /* ------------------------------------------------------------------------ */ /* * Food - something you can eat. By default, when an actor eats a food * item, the item disappears. */ class Food: Thing dobjFor(Taste) { /* tasting food is perfectly logical */ verify() { } } dobjFor(Eat) { verify() { } action() { /* describe the consumption */ defaultReport(&okayEatMsg); /* the object disappears */ moveInto(nil); } } ; /* ------------------------------------------------------------------------ */ /* * OnOffControl - a generic control that can be turned on and off. We * keep track of an internal on/off state, and recognize the commands * "turn on" and "turn off". */ class OnOffControl: Thing /* * The current on/off setting. We'll start in the 'off' position by * default. */ isOn = nil /* * On/off status name. This returns the appropriate name ('on' or * 'off' in English) for our current status. */ onDesc = (isOn ? gLibMessages.onMsg(self) : gLibMessages.offMsg(self)) /* * Change our on/off setting. Subclasses can override this to apply * any side effects of changing the value. */ makeOn(val) { /* remember the new value */ isOn = val; } dobjFor(TurnOn) { verify() { /* if it's already on, complain */ if (isOn) illogicalAlready(&alreadySwitchedOnMsg); } action() { /* set to 'on' and generate a default report */ makeOn(true); defaultReport(&okayTurnOnMsg); } } dobjFor(TurnOff) { verify() { /* if it's already off, complain */ if (!isOn) illogicalAlready(&alreadySwitchedOffMsg); } action() { /* set to 'off' and generate a default report */ makeOn(nil); defaultReport(&okayTurnOffMsg); } } ; /* * Switch - a simple extension of the generic on/off control that can be * used with a "switch" command without specifying "on" or "off", and * treats "flip" synonymously. */ class Switch: OnOffControl /* "switch" with no specific new setting - reverse our setting */ dobjFor(Switch) { verify() { } action() { /* reverse our setting and generate a report */ makeOn(!isOn); defaultReport(isOn ? &okayTurnOnMsg : &okayTurnOffMsg); } } /* "flip" is the same as "switch" for our purposes */ dobjFor(Flip) asDobjFor(Switch) ; /* ------------------------------------------------------------------------ */ /* * Settable - an abstract class for things you can set to different * settings; the settings can be essentially anything, such as numbers * (or other markers) on a dial, or stops on a sliding switch. */ class Settable: Thing /* * Our current setting. This is an arbitrary string value. The * value initially assigned here is our initial setting; we'll * update this whenever we're set to another setting. */ curSetting = '1' /* * Canonicalize a proposed setting. This ensures that the setting is * in a specific primary format when there are superficially * different ways of expressing the same value. For example, if the * setting is numeric, this could do things like trim off leading * zeros; for a text value, it could ensure the value is in the * proper case. */ canonicalizeSetting(val) { /* * by default, we don't have any special canonical format, so * just return the value as it is */ return val; } /* * Change our setting. This is always called with the canonical * version of the new setting, as returned by canonicalizeSetting(). * Subclasses can override this routine to apply any side effects of * changing the value. */ makeSetting(val) { /* remember the new value */ curSetting = val; } /* * Is the given text a valid setting? Returns true if so, nil if * not. This should not display any messages; simply indicate * whether or not the setting is valid. * * This is always called with the *canonical* value of the proposed * new setting, as returned by canonicalizeSetting(). */ isValidSetting(val) { /* * By default, allow anything; subclasses should override to * enforce our valid set of values. */ return true; } /* * "set <self>" action */ dobjFor(Set) { verify() { logicalRank(150, 'settable'); } action() { askForLiteral(SetTo); } } /* * "set <self> to <literal>" action */ dobjFor(SetTo) { preCond = [touchObj] verify() { local txt; /* * If we already know our literal text, and it's not valid, * reduce the logicalness. Don't actually make it * illogical, as it's probably still more logical to set a * settable to an invalid setting than to set something that * isn't settable at all. */ if ((txt = gAction.getLiteral()) != nil && !isValidSetting(canonicalizeSetting(txt))) logicalRank(50, 'invalid setting'); } check() { /* if the setting is not valid, don't allow it */ if (!isValidSetting(canonicalizeSetting(gAction.getLiteral()))) { /* there is no such setting */ reportFailure(setToInvalidMsgProp); exit; } } action() { /* set the new value */ makeSetting(canonicalizeSetting(gAction.getLiteral())); /* remark on the change */ defaultReport(okaySetToMsgProp, curSetting); } } /* our message property for an invalid setting */ setToInvalidMsgProp = &setToInvalidMsg /* our message property for acknowledging a new setting */ okaySetToMsgProp = &okaySetToMsg ; /* * Dial - something you can turn to different settings. Note that dials * are usually used as components of larger objects; since our base * class is the basic Settable, component dials should be created to * inherit multiply from Dial and Component, in that order. * * This is almost hte same as a regular Settable; the only thing we add * is that we make "turn <self> to <literal>" equivalent to "set <self> * to <literal>", as this is the verb most people would use to set a * dial. */ class Dial: Settable /* "turn" with no destination - indicate that we need a setting */ dobjFor(Turn) { verify() { illogical(&mustSpecifyTurnToMsg); } } /* treat "turn <self> to <literal>" the same as "set to" */ dobjFor(TurnTo) asDobjFor(SetTo) /* refer to setting the dial as turning it in our messages */ setToInvalidMsgProp = &turnToInvalidMsg okaySetToMsgProp = &okayTurnToMsg ; /* * Numbered Dial - something you can turn to a range of numeric values. */ class NumberedDial: Dial /* * The range of settings - the dial can be set to values from the * minimum to the maximum, inclusive. */ minSetting = 1 maxSetting = 10 /* * Canonicalize a proposed setting value. For numbers, strip off any * leading zeros, since these don't change the meaning of the value. */ canonicalizeSetting(val) { local num; /* try parsing it as a digit string or a spelled-out number */ if ((num = parseInt(val)) != nil) { /* * we parsed it successfully - return the string * representation of the numeric value */ return toString(num); } /* it didn't parse as a number, so just return it as-is */ return val; } /* * Check a setting for validity. A setting is valid only if it's a * number within the allowed range for the dial. */ isValidSetting(val) { local num; /* if it doesn't look like a number, it's not valid */ if (rexMatch('<digit>+', val) != val.length()) return nil; /* get the numeric value */ num = toInteger(val); /* it's valid if it's within range */ return num >= minSetting && num <= maxSetting; } ; /* * Labeled Dial - something you can turn to a set of arbitrary text * labels. */ class LabeledDial: Dial /* * The list of valid settings. Each entry in this list should be a * string value. We ignore the case of these labels (we convert * everything to upper-case when comparing labels). */ validSettings = [] /* * Canonicalize the setting. We consider case insignificant in * matching our labels, but the canonical version of a setting is the * one that appears in the validSettings list - so if the player * types in SET DIAL TO EXTRA LOUD, and the validSettings list * contains 'Extra Loud', we'll want to convert the 'EXTRA LOUD' to * the capitalization of the validSettings entry. */ canonicalizeSetting(val) { local txt; /* * convert it to upper-case, so that we can compare it to our * valid labels without regard to case */ txt = val.toUpper(); /* * if we find a match in the validSettings list, return the match * from the list, since that's the canonical format */ if ((txt = validSettings.valWhich({x: x.toUpper() == txt})) != nil) return txt; /* we didn't find a match, so leave the original value unchanged */ return val; } /* * Check a setting for validity. A setting is valid only if it * appears in the validSettings list for this dial. */ isValidSetting(val) { /* * If the given value appears in our validSettings list, it's a * valid setting; otherwise, it's not valid. Ignore case when * comparing values by converting the valid labels to upper case; * we've already converted the value we're testing to upper case, * so the case mix won't matter in our comparison. * * Note that we're handed a canonical setting value, so we don't * have to worry about case differences. */ return validSettings.indexOf(val) != nil; } ; /* ------------------------------------------------------------------------ */ /* * Button - something you can push to activate, as a control for a * mechanical device. */ class Button: Thing dobjFor(Push) { verify() { } action() { /* * individual buttons should override this to carry out any * special action for the button; by default, we'll just * show a simple acknowledgment */ defaultReport(&okayPushButtonMsg); } } ; /* ------------------------------------------------------------------------ */ /* * Lever - something you can push, pull, or move, generally as a control * for a mechanical device. Our basic lever has two states, "pushed" * and "pulled". */ class Lever: Thing /* * The current state. We have two states: "pushed" and "pulled". * We start in the pushed state, so the lever can initially be * pulled, since "pull" is the verb most people would first think to * apply to a lever. */ isPulled = nil /* * Set the state. This can be overridden to apply side effects as * needed. */ makePulled(pulled) { /* note the new state */ isPulled = pulled; } /* * Action handlers. We handle push and pull, and we treat "move" as * equivalent to whichever of push or pull is appropriate to reverse * the current state. */ dobjFor(Push) { verify() { /* if it's already pushed, pushing it again makes no sense */ if (!isPulled) illogicalAlready(&alreadyPushedMsg); } action() { /* set the new state to pushed (i.e., not pulled) */ makePulled(nil); /* make the default report */ defaultReport(&okayPushLeverMsg); } } dobjFor(Pull) { verify() { /* if it's already pulled, pulling it again makes no sense */ if (isPulled) illogicalAlready(&alreadyPulledMsg); } action() { /* set the new state to pulled */ makePulled(true); /* make the default report */ defaultReport(&okayPullLeverMsg); } } dobjFor(Move) { verify() { } check() { /* run the check for pushing or pulling, as appropriate */ if (isPulled) checkDobjPush(); else checkDobjPull(); } action() { /* if we're pulled, push the lever; otherwise pull it */ if (isPulled) actionDobjPush(); else actionDobjPull(); } } ; /* * A spring-loaded lever is a lever that bounces back to its starting * position after being pulled. This is essentially equivalent in terms * of functionality to a button, but can at least provide superficial * variety. */ class SpringLever: Lever dobjFor(Pull) { action() { /* * Individual objects should override this to perform the * appropriate action when the lever is pulled. By default, * we'll do nothing except show a default report. */ defaultReport(&okayPullSpringLeverMsg); } } ; /* ------------------------------------------------------------------------ */ /* * An item that can be worn */ class Wearable: Thing /* is the item currently being worn? */ isWorn() { /* it's being worn if the wearer is non-nil */ return wornBy != nil; } /* * make the item worn by the given actor; if actor is nil, the item * isn't being worn by anyone */ makeWornBy(actor) { /* remember who's wearing the item */ wornBy = actor; } /* * An item being worn is not considered to be held in the wearer's * hands. */ isHeldBy(actor) { if (isWornBy(actor)) { /* it's being worn by the actor, so it's not also being held */ return nil; } else { /* * it's not being worn by this actor, so use the default * interpretation of being held */ return inherited(actor); } } /* * A wearable is not considered held by an actor when it is being * worn, so we must do a what-if test for removing the item if the * actor is currently wearing the item. If the actor isn't wearing * the item, we can use the default test of moving the item into the * actor's inventory. */ whatIfHeldBy(func, newLoc) { /* * If the article is being worn, and it's already in the same * location we're moving it to, simply test with the article no * longer being worn. Otherwise, inherit the default handling. */ if (location == newLoc && wornBy != nil) return whatIf(func, &wornBy, nil); else return inherited(func, newLoc); } /* * Try making the current command's actor hold me. If I'm already * directly in the actor's inventory and I'm being worn, we'll try a * 'doff' command; otherwise, we'll use the default handling. */ tryHolding() { /* * Try an implicit 'take' command. If the actor is carrying the * object indirectly, make the command "take from" instead, * since what we really want to do is take the object out of its * container. */ if (location == gActor && isWornBy(gActor)) return tryImplicitAction(Doff, self); else return inherited(); } /* * The object wearing this object, if any; if I'm not being worn, * this is nil. The wearer should always be a container (direct or * indirect) of this object - in order to wear something, you must * be carrying it. In most cases, the wearer should be the direct * container of the object. * * The reason we keep track of who's wearing the object (rather than * simply keeping track of whether it's being worn) is to allow for * cases where an actor is carrying another actor. Since this * object will be (indirectly) inside both actors in such cases, we * would have to inspect intermediate containers to determine * whether or not the outer actor was wearing the object if we * didn't keep track of the wearer directly. */ wornBy = nil /* am I worn by the given object? */ isWornBy(actor) { return wornBy == actor; } /* * An article of clothing that is being worn by an actor does not * typically encumber the actor at all, so by default we'll return * zero if we're being worn by the actor, and our normal bulk * otherwise. */ getEncumberingBulk(actor) { /* * if we're being worn by the actor, we create no encumbrance at * all; otherwise, return our normal bulk */ return isWornBy(actor) ? 0 : getBulk(); } /* * An article of clothing typically encumbers an actor with the same * weight whether or not the actor is wearing the item. However, * this might not apply to all objects; a suit of armor, for * example, might be slightly less encumbering in terms of weight * when worn than it is when held because the distribution of weight * is more manageable when worn. By default, we simply return our * normal weight, whether worn or not; subclasses can override as * needed to differentiate. */ getEncumberingWeight(actor) { return getWeight(); } /* get my state */ getState = (isWorn() ? wornState : unwornState) /* my list of possible states */ allStates = [wornState, unwornState] /* -------------------------------------------------------------------- */ /* * Action processing */ dobjFor(Wear) { preCond = [objHeld] verify() { /* make sure the actor isn't already wearing the item */ if (isWornBy(gActor)) illogicalAlready(&alreadyWearingMsg); } action() { /* make the item worn and describe what happened */ makeWornBy(gActor); defaultReport(&okayWearMsg); } } dobjFor(Doff) { preCond = [roomToHoldObj] verify() { /* * Make sure the actor is actually wearing the item. If * they're not, it's illogical, but if they are, it's an * especially likely thing to remove. */ if (!isWornBy(gActor)) illogicalAlready(¬WearingMsg); else logicalRank(150, 'worn'); } action() { /* un-wear the item and describe what happened */ makeWornBy(nil); defaultReport(&okayDoffMsg); } } /* "remove <wearable>" is the same as "doff <wearable>" */ dobjFor(Remove) asDobjFor(Doff) /* * if a wearable is being worn, showing it off to someone doesn't * require holding it */ dobjFor(ShowTo) { preCond() { /* get the standard handling */ local lst = inherited(); /* if we're being worn, don't require us to be held */ if (isWornBy(gActor)) lst -= objHeld; /* return the result */ return lst; } } ; /* ------------------------------------------------------------------------ */ /* * An item that can provide light. * * Any Thing can provide light, but this class should be used for * objects that explicitly serve as light sources from the player's * perspective. Objects of this class display a "providing light" * status message in inventory listings, and can be turned on and off * via the isLit property. */ class LightSource: Thing /* is the light source currently turned on? */ isLit = true /* * Turn the light source on or off. Note that we don't have to make * any special check for a change to the light level, because the * main action handler always checks for a change in light/dark * status over the course of the turn. */ makeLit(lit) { /* change the status */ isLit = lit; } /* * We can distinguish light sources according to their isLit status. * Give the lit/unlit distinction higher priority than the normal * ownership/containment distinction. */ distinguishers = [basicDistinguisher, litUnlitDistinguisher, ownershipDistinguisher, locationDistinguisher] /* the brightness that the object has when it is on and off */ brightnessOn = 3 brightnessOff = 0 /* * return the appropriate on/off brightness, depending on whether or * not we're currently lit */ brightness { return isLit ? brightnessOn : brightnessOff; } /* get our current state: lit or unlit */ getState = (brightness > 1 ? lightSourceStateOn : lightSourceStateOff) /* get our set of possible states */ allStates = [lightSourceStateOn, lightSourceStateOff] ; /* * A Flashlight is a special kind of light source that can be switched * on and off. * * To create a limited-use flashlight (with a limited battery life, for * example), you can combine this class with FueledLightSource. The * flashlight's on/off switch status is a separate property from its * lit/unlit light-source status, so combining Flashlight with * FueledLightSource will actually allow the two to become decoupled: a * flashlight can be on without providing light, when the battery is * dead. For this reason, you might want to override the decription, * and possibly the TurnOn action() handler, to customize the messages * for the case when the flashlight is switched on but out of power. */ class Flashlight: LightSource, Switch /* our switch status - start in the 'off' position */ isOn = nil /* * Change the on/off status. Note that switching the flashlight on * or off should always be done via makeOn - the makeLit inherited * from the LightSource should never be called directly on a * Flashlight object, because it doesn't keep the switch on/off and * flashlight lit/unlit status in sync. This routine is the one to * call because it keeps everything properly synchronized. */ makeOn(stat) { /* inherit the default handling */ inherited(stat); /* * Set the 'lit' status to track the on/off status. Note that * we don't simply do this by deriving isLit from isOn because * we want to invoke the side effects of changing the status by * calling makeLit explicitly. We also want to allow the two to * be decoupled when necessary, such as might happen when the * flashlight's bulb is burned out, or its battery has run down. */ makeLit(stat); } /* initialize */ initializeThing() { /* inherit default handling */ inherited(); /* * Make sure our initial isLit setting (for the LightSource) * matches our initial isOn steting (for the Switch). The * switch status drives the light source status, so initialize * the latter from the former. */ isLit = isOn; } /* treat 'light' and 'extinguish' as 'turn on' and 'turn off' */ dobjFor(Light) asDobjFor(TurnOn) dobjFor(Extinguish) asDobjFor(TurnOff) /* if we turn on the flashlight, but it doesn't light, mention this */ dobjFor(TurnOn) { action() { /* do the normal work */ inherited(); /* * If we're now on but not lit, mention this. This can * happen when we run out of power in the battery, or our * bulb is missing or burned out, or we're simply broken. */ if (isOn && !isLit) mainReport(&flashlightOnButDarkMsg); } } ;
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