Extras

The adv3Lite library has been devised to minimize the number of classes a game author has to remember, so that a huge number of game objects can simply be defined as belonging to the Thing class. That may suit some game authors but not all. Authors used to the adv3 library may prefer to use the classes they are familiar with from there. Also, while using a greater number of classes means you have to remember more initially, it may make your code more readable in the long term, since assigning an object to an appropriate class may make it more immediately obvious what its function is.

To give game authors the choice of using a larger array of classes if they so wish, the extras.t module defines a number of classes that work more or less the same as the similarly named classes in adv3. Note that the 'shallow' classes add little or no real functionality to the adv3Lite library, however, since in most cases they simply define one or two properties that game authors could equally well define for themselves on Thing. There are, however, one or two classes in the extras.t module that do a little more than that (particularly the TravelConnector-derived classes), and it will usually prove better to make use of these 'deeper' classes than attempting to roll your own on Thing if you want their full functionality.

Shallow Equivalents to adv3 Classes

We start with the 'shallow' classes that simply define one or two properties on Thing (well, actually one or two of them do a bit more than that, but this will do as a first approximation):

Remember, you can use as many or as few of these classes as you wish. In practice, using these clases probably makes game code more readable, as well as providing the ability to write code that iteratees over objects of a given class or modifies one of these classes to customize it to the requirements of a particular game.

Some game authors may wish to exclude all Decorations from any command applied to ALL (such as X ALL or FEEL ALL), since in a location with several decorations. players may find it needlessly distracting, or at least cluttered, to read several messages along the lines of 'X is not important' in response to such commands. So a commonly useful modification to the Decoration class might be:

modify Decoration
    hideFromAll(action) { return true; }
;

This particular modification may be less useful in games that in any case define gameMain.allVerbsAllowAll = nil, but it illustrates the principle. It might nevertheless have been a good default to define in the library, but changing it in the library now would compromise backward compatibility.

TravelConnector Classes

The following classes implement one or two TravelConnector objects that are also present as physical objects in the game world:

These classes work just a little differently from their adv3 equivalents, in that you don't have to use them in pairs. To use one of the above classes, put an instance in the appropriate room, set its destination property to the room you want it to lead to, and set the appropriate direction property of the room to point to it. For example, to implement a flight of stairs leading up from the hall to the landing you might write:

hall: Room 'Hall'
    "A broad flight of stairs leads up to the landing above. "
    up = hallStairs
;

+ hallStairs 'flight[n] of stairs; broad; steps staircase'
     destination = landing
;

Each of the above classes has a double-sided (or two-way) equivalent to allow the two ends of a passage or stairway to be defined with a single object:

  • DSStairway Can be used for anything the player character might climb up from one end down from the other.
  • DSPassage: Can be used for anything the player character might enter or go through either end to reach the other side.
  • DSPathPassage: Like a DSPassage, except that following it also traverses it.
  • Objects of all these DS classes can be set up in the aame way as for a DSDoor. That is the locations they connect are defined on their room1 and room2 properties. In addition, the DSStairway class defines the properties upperEnd and lowerEnd (mainly to determine which end the actor can go down from and which up from). The library attempts to determine which end is which by inspecting the up and down properties of room1 and room2. Only if these provide no clue will it be necessary for a game author to specify upperEnd or lowerEnd manually (specifying one automatically specifies the other).

    These DS classes also provide most of the other double-sided (two-way) properties and methods available on DSDoor, including inRoom1, inRoom2, byRoom([room1str, room2str]), room1Desc, room2Desc, attachedDir(), doorDir() and dirName, for which see the description of the DSDoor class.

    Finally, there is also a DSTravelConnector which can be used to implement a two-way TravelConnector. This must be defined as a separate (i.e. not embedded) object with its room1 and room2 properties set to the two rooms it connects. In addition to all the normal TravelConnector properties and methods it also implements inRoom1, inRoom2, byRoom([room1str, room2str]), attachedDir(), doorDir() and dirName. The name use of this class would mostly likely to be to set up common travel barriers or travel descriptions on a two-way connection, e.g,

    hall: Room 'Hall'
        "There's a very narrow gap to the east."
         east = gapCon
    ;
    
    gapCon: DSTravelConnector @hall @parlour
       travelDesc = "You just manage to squeeze through the gap. "
       
       canTravelerPass(actor)
       {
           return actor.getCarriedBulk() < 3;
       }
       
       explainTravelBarrier()
       {
           "You can't squeeze through carrying all that stuff. ";
       }
    ;
    
    
    parlour:Room 'Parlour'
       "The only way out of this tiny parlour appears to be the narrow
        gap to the west. "
        west = gapCon
    ;
    

    Sensory Emanation Classes

    The adv3Lite library defines the classes Noise and Odor to represent a sound and a smell respectively. Users familiar with adv3 should note that these classes are much simpler than the adv3 classes with the same name (and are more like the adv3 SimpleNoise and SimpleOdor classes). They are simply Decoration objects that can be either listened to (for a Noise) or smelled (for an Odor), either of which is treated as the same as examining them. For anything else they respond with 'You can't do that to a noise|smell. ' Since they don't by default define smellDesc or listenDesc they won't normally respond to an intransitive SMELL or LISTEN command. They can, however, be used to provide simple implementations of any smells or sounds whose existence is suggested by smelling or listening to other objects, or by issuing an intransitive SMELL or LISTEN command. For example:

    + cooker: Thing 'cooker;blackened;oven stove top'
        "Normally, you keep it in pretty good shape (or your cleaner does) but right
        now it's looking suspiciously blackened, especially round the top. "    
        
        isFixed = true
        isSwitchable = true
        isOn = true
        
        smellDesc = "There's a distinct smell of burning from the cooker. "
    ;
    
    
    + Odor 'smell of burning; acrid distinct'
        "It smells quite acrid. "   
    ;
    

    As of Version 1.61, SensoryEmanations are hidden from any actions applied to ALL that are not relevant to the SensoryEmanation in questio (i.e. EXAMINE and either SMELL or LISTEN TO).

    If your game needs a more sophisticated handling of sounds and smells than these rather simple classes offer, you may want to consider including the Sensory extension.


    Other Miscellaneous Classes

    The Flashlight is a subclass of Switch that lights up when its switched on, and goes out when its switched off. It can also be switched on and off with LIGHT and EXTINGUISH.

    An Immovable is like a Fixture, except that taking it is blocked in check() rather than verify(). What this is means is that although it can't be taken, this doesn't affect the parser's choice of it as the target of a TAKE command. This could be used for anything that looks like it might be possible to take, but turns out not to be takeable (perhaps because it's heavier than it looks, or it's fastened in place but not obviously so). Note that while this means an Immovable can't be taken, it doesn't necessarily mean it can't be moved by other means, such as being pushed around by a PushTravel or MoveTo command. By default an Immovable is regarded as something that can't be moved (isMoveable = nil) unless it can be moved by PushTravel and or PullTravel (although this can, of course, be overridden in game code) since it would seem illogical to allow, e.g., PUSH THROUGH DOOR but not MOVE CRATE TO DOOR.

    An Unthing is an object that used to represent the absence of something that a player might assume to be present. The purpose of an Unthing is simply to provide a message explaining why the thing in question isn't there. For example, if the player drops a key down a drain, you could then add an Unthing to the location to remind the player why the key is no longer available:

    unKey: Unthing 'small silver key'
       'Unfortunately, you dropped the silver key down the drain. '
    ;   
    

    Note that the second property we're defining with the template here is not the description but the notHereMsg, and that this must be a single-quoted string. Any attempt to perform any action with an Unthing will result in the display of its notHereMsg.

    When choosing objects the parser will always prefer any other object to an Unthing. So, in the previous example, if the unKey was in scope at the same time as a large brass key, say, the parser will always choose the large brass key in respond to commands that just refer to a 'key', e.g. X KEY, TAKE KEY or UNLOCK DOOR WITH KEY.

    The Unthing class inherits from Decoration. You can make selected actions work with an Unthing by overriding is decorationActions property. E.g. if you'd defined a RETRIEVE command which you wanted to work on the UnKey (to make the player character try to fish the real silver key out of the drain, maybe), you could define your unKey thus:

    unKey: Unthing 'small silver key'
       'Unfortunately, you dropped the silver key down the drain. '
       
       decorationActions = [Retrieve]
    ;   
    

    A MinorItem is an unobtrusive and possibly unimportant portable object that's worth implementing in the game but sufficiently minor as to be not worth mentioning in response to X or FOO ALL unless it's either directly held by the player character or directly in the enclosing room or directly in the actor's location. Its includeTakeFromPutAll (which is true by default) determines whether it will be included in TAKE ALL FROM X or PUT ALL IN/ON/UNDER/BEHIND X when it's not directly held or directly in the enclosing room or actor's location.

    A CollectiveGroup can be used to represent a collection of objects for certain actions. It's normally best used as a Fixture representing other Fixtures (see below if you need it to work with mobile objects). To use a CollectiveGroup define an object of the CollectiveGroup class in a particular location, and then the objects the CollectiveGroup represents in the same location, defining the collectiveGroups property of each of those other objects to point to the CollectiveGroup. For example, suppose we have a bank of switches comprising a red switch, a blue switch and a green switch; in outline we might do something like this:

    + switchBank: CollectiveGroup 'switches; of[prep]; row bank; them'
       "The bank comprises a row of three switches: one red, one blue, one green. "
       collectiveActions = [Examine, Take]
    ;
    
    + redSwitch: Switch 'red switch'
        isFixed = true
        collectiveGroups = [switchBank]
    ;
    
    + blueSwitch: Switch 'blue switch'
        isFixed = true
        collectiveGroups = [switchBank]
    ;
    
    + greenSwitch: Switch 'green switch'
        isFixed = true
        collectiveGroups = [switchBank]
    ;
    

    With this in place the command X SWITCHES will give the description of the bank of switches, rather than of each individual switch, and TAKE SWITCHES will yield the message "The switches are fixed in place" rather than three messages to that effect, one for each switch. On the other hand the command FLIP SWITCHES will act on each of the individual switches in turn, since it's not one of the collectiveActions defined for the switchBank CollectiveGroup.

    Note the use of the collectiveActions property to define which actions will be handled by the CollectiveGroup rather than by each of its members. By default, collectiveActions is simply [Examine], but, as here, we can override it to contain other actions instead or as well.

    For this to work properly, the name section of the CollectiveGroup object should simply be the plural of the name common to each of its members (here 'switches' corresponding to 'switch').

    If the desc property of a CollectiveGroup is not explicitly defined, it defaults to a list of those of its members that are in scope.

    For some situations the Collective class (or its DispensingCollective subclass) defined in the Collective extension, may be a better bet than CollectiveGroup. The kind of situation where you'd want to use a Collective of DispensingCollective is where you have a group item (e.g. a bunch of grapes or stack of cans) from which you want to draw one or more individual items (e.g. grapes or cans).

    If you need a CollectiveGroup to represent items that are not fixed in place, but might be moved around (a collection of short portable cables, for example), you can instead use the MobileCollectiveGroup class defined in the MobileCollectiveGroup extension.


    A SecretDoor is a Door that only acts like a Door when it's open. When it's closed it's either totally invisible, or it appears to be something else, such as a bookcase or a panel.

    To use a SecretDoor, define it just like a Door, but (assuming it starts out closed) define its vocab property to be whatever's suitable for its closed state, and a separate vocabWhenOpen property to define the name and other vocab to use when it's open. For example:

    cellar: Room 'Cellar' 'cellar'
        "It's not a pleasant place at the best of times, dark, dank and smelly, with
        piles of old junk strewn all over the place waiting for you to find time to
        sort them out (which you probably never will). A wine rack stands <<unless
          wineRack.isOpen>> empty against the east wall<<else>>open to the east,
        revealing a passage beyond<<end>>. "
        
        isLit = nil
        darkName = 'Cellar (in the dark)'
        darkDesc = "It's too dark to see anything down here, but you could just
            about find your way back up to the kitchen. "
        up = kitchen
        west = wineRack
        
        regions = downstairs
    ;
    
    + wineRack: SecretDoor 'wine rack; empty'
        "It's empty; you never got round to restocking it. <<if isOpen>>It's also
        open, revealing a dark passage behind.<<end>> "
        
        afterAction()
        {
           if(gActionIs(Jump) && !isOpen)
            {
                "The vibration causes the wine rack to swing open, revealing a dark
                passage beyond. ";
                makeOpen(true);
            }
        
        }
        otherSide = dpDoor
        
        vocabWhenOpen = 'dark passage; empty wine; rack'
    ;
    

    Note that the OPEN command won't work on a SecretDoor when it's closed, but the CLOSE command will work on a SecretDoor when it's open (unless you override the isOpenable property to make it do otherwise). The default assumption is that a SecretDoor has to be opened by some non-standard and probably non-obvious means.

    If (exceptionally) a SecretDoor starts out open you can define its vocabWhenClosed property to specify the name and vocab to use for it when it's closed.

    If you want a SecretDoor to be effectively invisible when it's closed, you could give it a vocab property comprising an empty string and make sure nothing else mentions it when it's closed, but it's probably easier just to make it a Door and define isHidden = !isOpen, for example:

    loft: Room 'Hay Loft' 'hay loft'
        "There's not much up here, apart from a few stray strands of straw
        scattered across the bare boards. A ladder leads back down to the
        main part of the barn below. "
        down = ladderDown
        west = loftDoor
        
    ;
    
    + Decoration 'straw; stray of; strands'
    ;
        
    + Decoration 'bare boards;;them'
    ;
    
    + ladderDown: StairwayDown 'ladder'
        destination = barn
        
        dobjFor(Pull)
        {
            action()
            {
                if(loftDoor.isOpen)
                {
                    "Pulling the ladder causes the secret door to close again. ";
                    loftDoor.makeOpen(nil);
                }
                else
                {
                    "Pulling the ladder causes a secret door to open to the west,
                    revealing a small compartment beyond. ";
                    loftDoor.makeOpen(true);                
                }
            }
        }
    ;
    
    + loftDoor: Door 'small compartment;secret;door''
        "It looks only just big enough to enter. "
        otherSide = compartmentDoor   
        
        specialDesc = "A small compartment has opened up to the west. "
        useSpecialDesc = isOpen
        isHidden = !isOpen
    ;
    
    
    smallCompartment: Room 'Small Compartment' 'small compartment'
       "There's only just enough room to stand in here. "
        otherSide = loftDoor
        
        east = compartmentDoor
        out asExit(east)
    ;
    
    + compartmentDoor: Door 'small door'
        otherSide = loftDoor
    ;
    

    Note that in both these example, the west exit will only be displayed in the exit lister when the corresponding SecretDoor is open. When a SecretDoor is closed then, for travel purposes, it behaves as if there's no exit through it, even though it's attached to an exit property like a standard Door.

    A ContainerDoor, on the other hand, isn't really a door at all, but it can be used to represent the door of an openable container, such that opening, closing, locking and unlocking the ContainerDoor has the same effect as opening, closing, locking and unlocking the container. To use a ContainerDoor we must locate it in multipy-containing Thing that has an OpenableContainer defined on its remapIn property; we can't define a ContainerDoor directly as part of an openable container since the door would then be hidden inside the container when it was closed.

    So, for example, to define a cooker/oven we can put things in or on and which has a door we should do this:

    cooker: Fixture 'cooker;; oven stove'
       remapIn: SubComponent { isOpenable = true }
       remapOn: SubComponent { }
    ;
    
    + cookerDoor: ContainerDoor 'cooker door; oven stove'
    ;