# senseRegion.t | documentation

    #charset "us-ascii"
    #include "advlite.h"

    /*----------------------------------------------------------------------------*/
    /*
     *   senseRegion.t
     *
     *   This file defines the senseRegion class and the modifications to other
     *   classes needed to support it. It can be omitted from games that don't need
     *   the functionality it provides.
     *
     *   This file forms part of the adv3Lite library by Eric Eve (c) 2012, 2013
     *
     */

    /*
     *   A SenseRegion is a special kind of Region allowing sensory connection
     *   between rooms
     */
    class SenseRegion: Region

        /* Is it possible to see from one room to another in this SenseRegion? */
        canSeeAcross = true

        /*
         *   Is it possible to hear sounds (but not necessarily converse) in one
         *   room from another in this SenseRegion?
         */
        canHearAcross = true

        /*   Do smells travel from one room to another in this SenseRegion? */
        canSmellAcross = true

        /*
         *   By default actors have to be in the same room to be able to converse.
         *   Even if sound can travel from one location to another that doesn't
         *   necessarily mean that one could converse over that distance. The only
         *   exception might be where a senseRegion models a relatively small area,
         *   like two ends of a room.
         *
         *   Note that if canHearAcross is nil setting canTalkAcross to true will
         *   have no effect.
         */
        canTalkAcross = nil

        /*
         *   Are rooms in this SenseRegion close enough together to allow objects to
         *   be thrown from one room to another; by default we'll assume not.
         */
        canThrowAcross = nil

        /*
         *   Use this method to carry out some additional initialization useful to
         *   SenseRegions
         */
        setFamiliarRooms()
        {
            /* Carry out the inherited handling. */
            inherited();

            /*
             *   Also take the opportunity to build each room's list of
             *   sensory-connected rooms.
             */

            /* Go through each room in our room list */
            foreach(local rm in roomList)
            {
                /*
                 *   If we can see into remote rooms from here, append our list of
                 *   visible rooms to the other room's list of visible rooms,
                 *   excluding itself.
                 */
                if(canSeeAcross)
                    rm.visibleRooms = rm.visibleRooms.appendUnique(roomList - rm);

                /* And so on for the other senses */
                if(canHearAcross)
                    rm.audibleRooms = rm.audibleRooms.appendUnique(roomList - rm);

                if(canSmellAcross)
                    rm.smellableRooms = rm.smellableRooms.appendUnique(roomList - rm);

                if(canTalkAcross)
                    rm.talkableRooms = rm.talkableRooms.appendUnique(roomList - rm);

                if(canThrowAcross)
                    rm.throwableRooms = rm.throwableRooms.appendUnique(roomList - rm);

                rm.linkedRooms = rm.linkedRooms.appendUnique(roomList - rm);

                /*
                 *   Sort each room's sensory room lists in ascending orfer of their
                 *   remoteRoomListOrder(pov) from the pov of that room.
                 */
                rm.visibleRooms = rm.sortRoomSublist(rm.visibleRooms, rm);
                rm.audibleRooms = rm.sortRoomSublist(rm.audibleRooms, rm);
                rm.smellableRooms = rm.sortRoomSublist(rm.smellableRooms, rm);

            }

        }

        /*
         *   Add everything to scope for all the rooms that belong to this
         *   SenseRegion. We do this by sending a senseProbe into each of the rooms
         *   and adding what would be in scope for that probe.
         */
        addExtraScopeItems(action)
        {
            /* Carry out the inherited handling */
            inherited(action);

            /* Initialize a new vector for our extra scope items */
            local extraScope = new Vector(30);

            /* Go through every room in our list */
            foreach(local rm in roomList)
            {
                /* Move the scopeProbe_ object into the room */
                scopeProbe_.moveInto(rm);

                /*
                 *   Add to our scope everything that's in scope for our scopeProbe_
                 *   while it's in the other room.
                 */
                extraScope.appendAll(Q.scopeList(scopeProbe_).toList);
            }

            /*
             *   Restrict the extra scope items to those that the actor knows about.
             */
            extraScope = extraScope.subset({o: gActor.knowsAbout(o)} );

            /*
             *   Append our list of extra scope items to the action's scope list,
             *   removing any duplicates.
             */
            action.scopeList = action.scopeList.appendUnique(extraScope.toList -
                scopeProbe_);

            /* Remove the scopeProbe_ from the map */
            scopeProbe_.moveInto(nil);
        }

        /*
         *   Since the PC should be able to see round the whole of a SenseRegion,
         *   presumably the PC should also be able to find their way around a
         *   SenseRegion once any of its rooms has been visited, even if the PC is
         *   yet to visit the other rooms. We can achieve this effect by making the
         *   SenseRegion familiar once any of its rooms has been visited, provided
         *   there's line of sight across the rooms in the SenseRegion and the PC is
         *   in a room in the SenseRegion which has light.
         */
        familiar()
        {
            local fam = nil;
            local loc = gPlayerChar.getOutermostRoom;

            if(canSeeAcross && loc.isIn(self) && loc.isIlluminated)
                fam = true;

            /*
             *   Once our familiar property becomes true it should never become nil
             *   again, so we can simply convert familiar to the constant value true
             *   and avoid the need to keep recalculating it.
             */
            if(fam)
                familiar = fam;

            return fam;
        }

        /*
         *   Flag: do we want the touchObj PreCondition to move the actor from one
         *   Room in the SenseRegion to another by implicit action to allow an actio
         *   to proceed? By default we set this to the value of contSpace, since if
         *   contSpace is true (i.e. we're using this SenseRegion to model a fairly
         *   small continuous space, such as a room) it probably makes sense for
         *   autoGoTo to be true as well, but it's nonetheless perfectly all right
         *   to override the values of autoGoTo and contSpace independently.
         */
        autoGoTo = contSpace

        /*
         *   If our autoGoTo flag is set, our fastGoTo flag should normally also be
         *   set so that autoGoTo works smoothly without breaks for CONTINUE
         *   commands impeding the workings of an implicit GoTo. Note, however, that
         *   it's perfectly okay to override fastGoTo to true on a SenseRegion for
         *   which autoGoTo is nil; it just may not be such a good idea to do it the
         *   other way round.
         */
        fastGoTo = autoGoTo

        /*
         *   Flag, do we want this SenseRegion to act as a continuous space (e.g.
         *   representing parts of the same room, so that moving from one room to
         *   another within this SenseRegion does not trigger a new room
         *   description, thereby creating the impression that we have moved within
         *   the same room rather than to another. This should probably be used
         *   sparingly and only for SenseRegions representing a relatively small
         *   area, otherwise the lack of a new room description (i.e. a LOOK AROUND)
         *   could be confusing to the player.
         */
        contSpace = nil

    ;

    /*
     *   modifications to Room to allow SenseRegions to work.
     */
    modify Room

        /*
         *   The list of rooms that are visible from this room. Ordinarily this list
         *   is constructed at Preinit by any Sense Regions this room belongs to, so
         *   shouldn't normally be manually adjusted by game code. It's conceivable
         *   that game code could tweak these lists after Preinit, though, perhaps
         *   to create a one-way connection (e.g. to model a high room that
         *   overlooks lower ones)
         */
        visibleRooms = []

        /*
         *   The lists of rooms we can smell, hear, talk or throw from/into from
         *   this room.
         */
        audibleRooms = []
        smellableRooms = []
        talkableRooms = []
        throwableRooms = []

        /*
         *   The list of rooms to which we're linked by virtue of being in the same
         *   SenseRegion.
         */
        linkedRooms = []

        /*
         *   Show the specialDescs of any items in the other rooms in our
         *   SenseRegions, where specialDescBeforeContents is true
         */
        showFirstConnectedSpecials(pov)
        {
            foreach(local rm in visibleRooms)
                rm.showFirstRemoteSpecials(pov);
        }

        /*
         *   Show the specialDescs of any items in the other rooms in our
         *   SenseRegions, where specialDescBeforeContents is nil
         */
        showSecondConnectedSpecials(pov)
        {
            foreach(local rm in visibleRooms)
                rm.showSecondRemoteSpecials(pov);
        }

        /* List the miscellaneous contents of a remote room */
        showConnectedMiscContents(pov)
        {
            foreach(local rm in visibleRooms)
                rm.showRemoteMiscContents(pov);
        }

        /*
         *   These properties are for the internal use of the remote listing
         *   routines, and should normally be left alone by game code.
         */
        remoteSecondSpecialList = nil
        remoteMiscContentsList = nil

        /*
         *   In additional to showing the first (i.e. pre-miscellaneous) list of
         *   items with specialDescs in remote locations, the
         *   showFirstRemoteSpecials() method builds the other lists of objects for
         *   the subsequent methods to use. pov is the point of view object
         *   (typically the player character) from whose point of view the list is
         *   being constructed.
         */
        showFirstRemoteSpecials(pov)
        {
            /*
             *   The list of items whose specialDescs are to be displayed before the
             *   list of miscellaneous items.
             */
            local specialVec1 = new Vector(10);

            /*
             *   The list of items whose specialDescs are to be displayed after the
             *   list of miscellaneous items.
             */
            local specialVec2 = new Vector(10);

            /*   The list of miscellaneous items */
            local miscVec = new Vector(10);

            /*
             *   Reduce our list to that subset of the list that is visible to the
             *   pov object.
             */
            local lst = contents.subset({o: o.isVisibleFrom(pov) && !o.isHidden});

            /*
             *   Sort the objects to be listed into three separate lists: those with
             *   specialDescs to be shown before the lists of miscellaneous items,
             *   the list of miscellaneous items, and the list of objects with
             *   specialDescs to be listed after the miscellaneous items.
             */
            foreach(local obj in lst)
            {
                /*
                 *   See if obj defines an initSpecialDesc property or specialDesc
                 *   property that is currently in use.
                 */
                if((obj.propType(&initSpecialDesc) != TypeNil &&
                   obj.useInitSpecialDesc()) ||
                   (obj.propType(&specialDesc) != TypeNil && obj.useSpecialDesc()))
                {
                    /*
                     *   If so, and obj's specialDesc is to be shown before any
                     *   miscellaneous contents, add obj to the first specials list
                     */
                    if(obj.specialDescBeforeContents)
                        specialVec1.append(obj);
                    /*
                     *   If obj's specialDesc is to be shown after this list of
                     *   miscellaneous context, add obj to the second list of
                     *   specials.
                     */
                    else
                        specialVec2.append(obj);
                }
                /*
                 *   Otherwise, if obj doesn't define a currently usable specialDesc
                 *   or initSpecialDesc, add it to the list of miscellaneous items.
                 */
                else
                    miscVec.append(obj);
            }

            /* Sort the first list of specials in specialDescOrder */
            specialVec1.sort(nil, {a, b: a.specialDescOrder - b.specialDescOrder});

            /* Sort the second list of specials in specialDescOrder */
            specialVec2.sort(nil, {a, b: a.specialDescOrder - b.specialDescOrder});

            /*
             *   Show the items in the first list, i.e. the list of items with
             *   specialDescs to be shown before the miscellaneous items.
             */
            foreach(local obj in specialVec1)
            {
                obj.showRemoteSpecialDesc(pov);
                listRemoteContents(obj.contents, remoteSubContentsLister, pov);
            }

            /* Store the other two lists for later use by other methods. */
            remoteSecondSpecialList = specialVec2.toList();
            remoteMiscContentsList = miscVec.toList().subset({o:
                o.location.ofKind(Room)});
        }

        /* Show the removeSpecialDesc of each item in the second list of specials */
        showSecondRemoteSpecials(pov)
        {
            foreach(local obj in remoteSecondSpecialList)
            {
                obj.showRemoteSpecialDesc(pov);
                listRemoteContents(obj.contents, remoteSubContentsLister, pov);
            }
        }

        /* List the miscellaneous list of items in this remote location */
        showRemoteMiscContents(pov)
        {
            remoteContentsLister.show(remoteMiscContentsList, inRoomName(pov));
            listRemoteContents(remoteMiscContentsList, remoteSubContentsLister,
                               pov);
        }

        /*
         *   The contents lister to use to list this room's miscellaneous contents
         *   when viewed from a remote location.
         */
        remoteContentsLister = remoteRoomContentsLister

        /*
         *   Reset the contents of all the remote rooms visible from this room to
         *   not having been mentioned.
         */
        unmentionRemoteContents()
        {
            foreach(local rm in visibleRooms)
                unmention(rm.allContents);
        }

        /*
         *   The name that's used to introduce a list of miscellaneous objects in
         *   this room when viewed from a remote location containing the pov object
         *   (normally the player character).
         */
        inRoomName(pov)
        {
            return BMsg(in room name, 'in {1}', theName);
        }

        /*
         *   The following six methods take effect only if there would otherwise be
         *   a sensory connection between the current room and loc due to their
         *   being in the same SenseRegion.
         *
         *   Can we see into this room from loc?
         */
        canSeeInFrom(loc) { return canSeeOutTo(loc); }

        /*   Can we see out of this room to loc? */
        canSeeOutTo(loc) { return true; }

        /*
         *   Can we hear into this room from loc (i.e. can an actor in loc hear
         *   something in this room?
         */
        canHearInFrom(loc) { return canHearOutTo(loc); }

        /*
         *   Can we hear out from this room to loc (i.e. can an actor in this room
         *   hear something in loc)?
         */
        canHearOutTo(loc) { return true; }

        /*
         *   Can we smell into this room from loc (i.e. can an actor in loc smell
         *   something in this room?
         */
        canSmellInFrom(loc) { return canSmellOutTo(loc); }

        /*
         *   Can we hear out from this room to loc (i.e. can an actor in this room
         *   hear something in loc)?
         */
        canSmellOutTo(loc) { return true; }

        /*
         *   Should we look around on entering this room? Normally we should, unless
         *   both the room obj is travelling from and the room it's travelling to
         *   (normally this room) both belong in a SenseRegion whose contSpace
         *   property is true.
         */
        lookOnEnter(obj)
        {
            return obj.getOutermostRoom.regionsInCommonWith(destination).indexWhich(
                { reg: reg.contSpace } ) == nil;
        }

        /*
         *   listOrder is inherited from Thing with a default value of 100. For a Room in a SenseRegion
         *   it can control the order in which other rooms in the SenseRegion have their contents
         *   described (higher = later), although this can be adjusted via the remoteRoomListOrder(pov)
         *   method.
         */
        // listOrder = 100

        /*
         *   For a Room in a SenseRegion return the order in which other rooms in the SenseRegion have
         *   their contents described (higher = later) from the point of view of the pov object, which
         *   will normally be the room from which the viewing, smelling or listening is being performed.
         *   By default we just return the Room's listOrder.
         */
        remoteRoomListOrder(pov)
        {
            return listOrder;
        }

        /*
         *   Sort a sublist of rooms in ascending order of their remoteRoomListOrder(pov) and return the
         *   result.
         */
        sortRoomSublist(sublist, pov)
        {
            /*
             *   If our sublist contains less than two elements return our sublist unchanged.
             */
            if(sublist.length < 2)
                return sublist;

            return sublist.sort(nil, {a, b: a.remoteRoomListOrder(pov) - b.remoteRoomListOrder(pov) });
        }

    ;

    /*
     *   The default Lister for listing miscellaneous objects in a remote location.
     */
    remoteRoomContentsLister: ItemLister
        /* is the object listed in a LOOK AROUND description? */
        listed(obj) { return obj.lookListed && !obj.isHidden; }

        /*
         *   Show the list prefix. The irName parameter is the inRoomName(pov)
         *   passed from Room.showRemoteMiscContents(pov).
         */
        showListPrefix(lst, pl, irName)
        {
            DMsg(remote contents prefix, '<.p>\^{1} {i} {see} ', irName);
        }

        showListSuffix(lst, pl, irName)
        {
            DMsg(remote contents suffix, '. ');
        }

        contentsListedProp = &contentsListedInLook
    ;

    /*
     *   The default Lister for listing the miscellaneous contents of objects in a
     *   remote location.
     */
    remoteSubContentsLister: ItemLister
        /* is the object listed in a LOOK AROUND description? */
        listed(obj) { return obj.lookListed && !obj.isHidden; }

        /*
         *   Show the list prefix. The irName parameter is the inRoomName(pov)
         *   passed from Room.showRemoteMiscContents(pov).
         */
        showListPrefix(lst, pl, inParentName)
        {
            DMsg(remote subcontents prefix, '<.p>\^{1} <<pl ? '{plural}
                    {is}' : '{dummy} {is}'>> ', inParentName);
        }

        showListSuffix(lst, pl, irName)
        {
            DMsg(remote subcontents suffix, '. ');
        }

        contentsListedProp = &contentsListedInLook
    ;

    /*
     *   Modifications to Thing to support the other mods required for use with
     *   SenseRegion.
     */
    modify Thing

       /*
        *   Show our remoteSpecialDesc, i.e. the version of our specialDesc that
        *   should be seen when this item is viewed from a remote location.
        */
        showRemoteSpecialDesc(pov)
        {
            /* If we've already been mentioned, don't do anything */
            if(mentioned)
                return;
            /*
             *   Otherwise note that we've now been mentioned before doing anything
             *   else.
             */
            else
                mentioned = true;

            /*
             *   If we have a non-nil initSpecialDesc and our useInitSpecialDesc
             *   property is true, show our remoteInitSpecialDesc from pov's point
             *   of view.
             */
            if(propType(&initSpecialDesc) != TypeNil && useInitSpecialDesc)
                remoteInitSpecialDesc(pov);

            /* Otherwise show our remoteSpecialDesc() */
            else
                remoteSpecialDesc(pov);

            /* Then add a paragraph break */
            "<.p>";

            /* Note that we've been seen */
            noteSeen();
        }

        /* List contents as seen from a remote location */
        listRemoteContents(lst, lister, pov)
        {
            local contList = lst.subset({o: o.isVisibleFrom(pov) && !o.isHidden});

            /*
             *   Sort the contList in listOrder. Although we're listing the contents
             *   of each item in the contList, it seems good to mention each item's
             *   contents in the listOrder order of the item. Amongst other things
             *   this helps give a consistent ordering for the listing of
             *   SubComponents.
             */
            contList = contList.sort(nil, {a, b: a.listOrder - b.listOrder});

            foreach(local obj in contList)
            {
                 /* Note that the object has been seen by the pc. */
                obj.noteSeen();

                /*
                 *   Don't list the inventory of any actors, or of any items that
                 *   don't want their contents listed, or any items we can't see in,
                 *   or of any items that don't have any contents to list.
                 */
                if(obj.contType == Carrier
                   || obj.(lister.contentsListedProp) == nil
                   || obj.canSeeIn() == nil
                   || obj.contents.length == 0)
                    continue;

                /*
                 *   Don't list any items that have already been mentioned or which
                 *   aren't visible from the pov.
                 */
                local objList = obj.contents.subset({x: x.mentioned == nil &&
                                                    x.isVisibleFrom(pov)});

                /*
                 *   Extract the list of items that have active specialDescs or
                 *   initSpecial Descs
                 */
                local firstSpecialList = objList.subset(
                    {o: (o.propType(&specialDesc) != TypeNil && o.useSpecialDesc())
                    || (o.propType(&initSpecialDesc) != TypeNil &&
                        o.useInitSpecialDesc() )
                    }
                    );

                /*
                 *   Remove items with specialDescs or initSpecialDescs from the
                 *   list of miscellaneous items.
                 */
                objList = objList - firstSpecialList;

                /*
                 *   From the list of items with specialDescs, extract those whose
                 *   specialDescs should be listed after any miscellaneous items
                 */
                local secondSpecialList = firstSpecialList.subset(
                    { o: o.specialDescBeforeContents == nil });

                /*
                 *   Remove the items whose specialDescs should be listed after the
                 *   miscellaneous items from the list of all items with
                 *   specialDescs to give the list of items with specialDescs that
                 *   should be listed before the miscellaneous items.
                 */
                firstSpecialList = firstSpecialList - secondSpecialList;

                /*
                 *   Sort the list of items with specialDescs to be displayed before
                 *   miscellaneous items by specialDescOrder
                 */
                firstSpecialList = firstSpecialList.sort(nil, {a, b: a.specialDescOrder -
                    b.specialDescOrder});

                /*
                 *   Sort the list of items with specialDescs to be displayed after
                 *   miscellaneous items by specialDescOrder
                 */
                secondSpecialList = secondSpecialList.sort(nil, {a, b: a.specialDescOrder -
                    b.specialDescOrder});

                /*
                 *   Show the specialDescs of items whose specialDescs should be
                 *   shown before the list of miscellaneous items.
                 */
                foreach(local cur in firstSpecialList)
                    cur.showRemoteSpecialDesc(pov);

                /*   List the miscellaneous items */
                if(objList.length > 0)
                {
                    lister.show(objList, obj.remoteObjInName(pov),
                                paraBrksBtwnSubcontents);
                    objList.forEach({o: o.mentioned = true });
                }

                /*
                 *   If we're not putting paragraph breaks between each subcontents
                 *   listing sentence, insert a space instead.
                 */
                if(!paraBrksBtwnSubcontents)
                    " ";

                /*
                 *   Show the specialDescs of items whose specialDescs should be
                 *   shown after the list of miscellaneous items.
                 */
                foreach(local cur in secondSpecialList)
                    cur.showRemoteSpecialDesc(pov);

                /*
                 *   Recursively list the contents of each item in this object's
                 *   contents, if it has any; but don't list recursively for an
                 *   object that's just been opened.
                 */
                if(obj.contents.length > 0 && lister != openingContentsLister)
                    listRemoteContents(obj.contents, lister, pov);

            }

        }

        /*
         *   Our remoteSpecialDesc is the paragraph describing this item in a room
         *   description when viewed from a remote location containing the pov
         *   object. By default we just show our ordinary specialDesc, but in
         *   practice we'll normally want to override this.
         */
        remoteSpecialDesc(pov) { specialDesc; }

        /*
         *   Our remoteInitSpecialDesc, used when viewing this item from a remote
         *   location.By default we just show our ordinary initSpecialDesc, but in
         *   practice we'll normally want to override this.
         */
        remoteInitSpecialDesc(pov) { initSpecialDesc; }

        /*
         *   Is this item visible from the remote location containing pov? By
         *   default it is if it's sightSize is not small, but this can be
         *   overridden, for example to vary with the pov.
         */
        isVisibleFrom(pov) { return sightSize != small; }

        /*
         *   Is this item audible from the remote location containing pov? By
         *   default it is if it's soundSize is not small, but this can be
         *   overridden, for example to vary with the pov.
         */
        isAudibleFrom(pov) { return soundSize != small; }

        /*
         *   Is this item smellable from the remote location containing pov? By
         *   default it is if it's smellSize is not small, but this can be
         *   overridden, for example to vary with the pov.
         */
        isSmellableFrom(pov) { return smellSize != small; }

        /*
         *   Assuming this item is readable at all, is it readable from the remote
         *   location containing pov? By default we assume this is the case if and
         *   only if the item's sightSize is large, but this can be overridden, for
         *   example for a large item with small lettering.
         */
        isReadableFrom(pov) { return sightSize == large; }

        /*
         *   The sightSize can be small, medium or large and controls how visible
         *   this object is from a remote location. If it's small, it can't be seen
         *   from a remote location at all. It it's medium, the object can be seen,
         *   but it's not possible to discern any detail. If it's large, it can be
         *   seen and described. Note that this behaviour is only the default,
         *   however, since it can be changed by overriding the isVisibleFrom() and
         *   remoteDesc() methods. Note also that sightSize is not related to the
         *   bulk property (unless you override sightSize to make it so).
         */
        sightSize = medium

        /*
         *   The soundSize can be small, medium or large. By default something is
         *   audible from a remote location either if its soundSize is large or if
         *   it soundSize is not small and its remoteListenDesc() method has been
         *   defined. Overriding isAudibleFrom(pov) may change these rules.
         */
        soundSize = medium

        /*
         *   The smellSize can be small, medium or large. By default something is
         *   smellable from a remote location either if its smellSize is large or if
         *   it smellSize is not small and its remoteSmellDesc() method has been
         *   defined. Overriding isSmellableFrom(pov) may change these rules.
         */
        smellSize = medium

        /*
         *   The following three properties only take effect on objects on which
         *   they're explicitly defined.
         */

        /*
         *   Define the remoteDesc() property to define the description of this item
         *   as it should be displayed when the item is examined from the remote
         *   location containing the pov object. Note that defining this property
         *   nullifies the distinction between a medium and a large sightSize, since
         *   the remoteDesc will be used in either case.
         */
    //    remoteDesc(pov) { desc; }

        /*
         *   Define the remoteListenDesc() property to define the description of
         *   this item as it should be displayed when the item is listened to from
         *   the remote location containing the pov object. Note that defining this
         *   property nullifies the distinction between a medium and a large
         *   soundSize, since the remoteListenDesc will be used in either case.
         */
    //    remoteListenDesc(pov) { listenDesc; }

        /*
         *   Define the remoteSmellDesc() property to define the description of this
         *   item as it should be displayed when the item is smelled from the remote
         *   location containing the pov object. Note that defining this property
         *   nullifies the distinction between a medium and a large smellSize, since
         *   the remoteSmellDesc will be used in either case.
         */
    //    remoteSmellDesc(pov) { smellDesc; }

        /*
         *   The name given to this object when it's the container for another
         *   object viewed remotely, e.g. 'in the distant bucket' as opposed to just
         *   'in the bucket'. By default we just use the objInName.
         */
        remoteObjInName(pov) { return objInName; }

        /*
         *   Modify the effect of Examine to show the remoteDesc if appropriate, or
         *   else our regular desc if our sightSize is large, or else a message to
         *   say we're too far away to see any detail. If we're in the same room as
         *   the actor, simply carry out the regular (inherited) method.
         */
        dobjFor(Examine)
        {
            action()
            {
                /*
                 *   If the actor doing the examining is in the same room as this
                 *   object, simply carry out the inherited handling.
                 */
                if(isIn(gActor.getOutermostRoom))
                    inherited;

                /* Otherwise, if we're being examined from a remote location... */
                else
                {
                    /*
                     *   If this Thing defines the remoteDesc() property, display
                     *   our remoteDesc() from the pov of the examining actor.
                     */
                    if(propDefined(&remoteDesc))
                    {
                        remoteDesc(gActor);
                        "<.p>";
                    }

                    /*
                     *   Otherwise if our sightSize is large, carry out the standard
                     *   (inherited) handling
                     */
                    else if(sightSize == large)
                        inherited;

                    /*
                     *   Otherwise say this object is too far away for the actor to
                     *   see any detail.
                     */
                    else
                    {
                        say(tooFarAwayToSeeDetailMsg);
                        "<.p>";
                    }
                }
            }
        }

        tooFarAwayToSeeDetailMsg = BMsg(too far away to see detail, '{The subj dobj}
            {is} too far away to make out any detail. ')

        /*
         *   Modify the effect of ListenTo to show the remoteListenDesc if
         *   appropriate, or else our regular listenDesc if our soundSize is large,
         *   or else a message to say we're too far away to hear. If we're in the
         *   same room as the actor, simply carry out the regular (inherited)
         *   method.
         */
        dobjFor(ListenTo)
        {
            action()
            {
                /*
                 *   If the actor doing the listening is in the same room as this
                 *   object, simply carry out the inherited handling.
                 */
                if(isIn(gActor.getOutermostRoom))
                    inherited;

                /* Otherwise, if we're being listened to from a remote location... */
                else
                {
                    /*
                     *   If this Thing defines the remoteListenDesc() property,
                     *   display our remoteListenDesc() from the pov of the
                     *   examining actor.
                     */
                    if(propDefined(&remoteListenDesc))
                    {
                        remoteListenDesc(gActor);
                        "<.p>";
                    }

                    /*
                     *   Otherwise if our soundSize is large, carry out the standard
                     *   (inherited) handling
                     */
                    else if(soundSize == large)
                        inherited;

                    /*
                     *   Otherwise say this object is too far away for the actor to
                     *   hear.
                     */
                    else
                    {
                        say(tooFarAwayToHearMsg);
                        "<.p>";
                    }
                }
            }
        }

        tooFarAwayToHearMsg = BMsg(too far away to hear, '{The subj dobj} {is} too
            far away to hear distinctly. ')

        /*
         *   Modify the effect of a Read action to prevent this item being read from
         *   a remote location unless isReadableFrom(gActor) is true.
         */
        dobjFor(Read)
        {
            verify()
            {
                inherited;

                if(!isIn(gActor.getOutermostRoom) && !isReadableFrom(gActor))
                    illogicalNow(tooFarAwayToReadMsg);
            }
        }

        tooFarAwayToReadMsg = BMsg(too far away to read, '{The subj dobj} {is} too
            far away to read. ')

        /*
         *   Modify the effect of SmellSomething to show the remoteSmellDesc if
         *   appropriate, or else our regular smellDesc if our smellSize is large,
         *   or else a message to say we're too far away to smell. If we're in the
         *   same room as the actor, simply carry out the regular (inherited)
         *   method.
         */
        dobjFor(SmellSomething)
        {
            action()
            {
                /*
                 *   If the actor doing the smelling is in the same room as this
                 *   object, simply carry out the inherited handling.
                 */
                if(isIn(gActor.getOutermostRoom))
                    inherited;

                /* Otherwise, if we're being smelled from a remote location... */
                else
                {
                    /*
                     *   If this Thing defines the remoteSmellDesc() property,
                     *   display our remoteSmellDesc() from the pov of the
                     *   examining actor.
                     */
                    if(propDefined(&remoteSmellDesc))
                    {
                        remoteSmellDesc(gActor);
                        "<.p>";
                    }

                    /*
                     *   Otherwise if our smellSize is large, carry out the standard
                     *   (inherited) handling
                     */
                    else if(smellSize == large)
                        inherited;

                    /*
                     *   Otherwise say this object is too far away for the actor to
                     *   smell.
                     */
                    else
                    {
                        say(tooFarAwayToSmellMsg);
                        "<.p>";
                    }
                }
            }
        }

        tooFarAwayToSmellMsg = BMsg(too far away to smell, '{The subj dobj} {is} too
            far away to smell distinctly. ')

        /* Modify the effects of throwing something at this object */
        iobjFor(ThrowAt)
        {
            action()
            {
                /*
                 *   If the Query object reckons that the throw is possible, carry
                 *   out the inherited handling.
                 */
                if(Q.canThrowTo(gActor, self))
                    inherited;

                /*
                 *   Otherwise move the direct object to the throwing actor's room
                 *   and display a message saying it fell short of its target.
                 */
                else
                {
                    gDobj.moveInto(gActor.getOutermostRoom);
                    say(throwFallsShortMsg);
                }
            }
        }

        /*
         *   For certain actions like EXAMINE there's no point trying to carry them
         *   out on something that can't be seen (remotely) if there are other
         *   things that can be seen that matched the player's command.
         */
        filterResolveList(np, cmd, mode)
        {
            inherited(np, cmd, mode);

            local loc = gActor.getOutermostRoom, prop;

            /*
             *   If we haven't been picked up by a general command that may not have
             *   been intended to apply specifically to us, or we're in the same
             *   room as the actor, we don'd need to do any filtering, so stop here.
             */
            if(mode != All || isIn(loc))
                return;

            /*
             *   Determine which precondition property we need to look at according
             *   to our object role.
             */
            switch(np.role)
            {
            case DirectObject:
                prop = cmd.action.preCondDobjProp;
                break;
            case IndirectObject:
                prop = cmd.action.preCondIobjProp;
                break;
            case AccessoryObject:
                prop = cmd.action.preCondIobjProp;
                break;
            default:
                prop = cmd.action.preCondDobjProp;
            }

            /*
             *   If the current action requires the object to be visible, and we're
             *   not in the same room as the actor, and the actor can't see us, but
             *   the actor can see something else among the possible matches, remove
             *   us from the list of matches.
             */
            if(valToList(self.(prop)).indexOf(objVisible)
               && !gActor.canSee(self)
               && np.matches.indexWhich({m: gActor.canSee(m.obj)}))
               np.matches = np.matches.subset({m: m.obj != self});

            /*
             *   If the current action requires the object to be touchabble, and
             *   we're not in the same room as the actor, and the actor can't reach
             *   us, but the actor can reach something else among the possible
             *   matches, remove us from the list of matches.
             */
            if(valToList(self.(prop)).indexOf(touchObj)
               && np.matches.indexWhich({m: m.obj.isIn(loc)}))
               np.matches = np.matches.subset({m: m.obj != self});

        }
    ;

    /*
     *   The scopeProbe_ is a dummy object used by the SenseRegion class to add
     *   items from Pther rooms in the SenseRegion to scope.
     */
    scopeProbe_: Thing
    ;

    /*
     *   Modifications to the (intransitive) Smell and Listen actions to list remote
     *   smells and sounds
     */
    modify Smell
        /*
         *   Return a list of items in remote locations that can be smelt from the
         *   current actor's location.
         */
        getRemoteSmellList()
        {
            /* Note the actor's location */
            local loc = gActor.getOutermostRoom;

            /* Create a new vector */
            local vec = new Vector(10);

            /*
             *   Go through all the rooms that can be smelled from the actor's
             *   location
             */
            foreach(local rm in loc.smellableRooms)
            {

                /*
                 *   Create a list of objects that are in rm that can be smelt by
                 *   the actor.
                 */
                local sList = rm.allContents.subset({o: Q.canSmell(gActor, o)});

                /*
                 *   Create a sublist of that list containing all the items with a
                 *   large smellSize that also define a non-nil smellDesc, plus all
                 *   the items that define a remoteSmellDesc, in each case provided
                 *   that the item's isProminentSmell is true. Remove any item (i.e.
                 *   MultiLoc) that's also in the actor's location, so that it's not
                 *   duplicated. Then append this sublist to our vector.
                 */
                vec.appendUnique(sList.subset({o: ((o.smellSize == large &&
                                                o.checkDisplay(&smellDesc) != nil) ||
                                           o.propDefined(&remoteSmellDesc))
                                           && o.isProminentSmell
                                              && !o.isOrIsIn(loc)} ));
            }

            /* Convert the vector to a list and return it. */
            return vec.toList();
        }

        /* List smells in remote locations */
        listRemoteSmells(lst)
        {
            local somethingDisplayed = nil;
            local remoteDisplayed = nil;

            /* For each item in lst */
            foreach(local cur in lst)
            {
                /* If the item defined a remoteSmellDesc property, display it */
                if(cur.propDefined(&remoteSmellDesc))
                {
                    remoteDisplayed = gOutStream.watchForOutput(
                        {:
                        cur.remoteSmellDesc(gActor)
                        });

                    if(remoteDisplayed)
                    {
                        somethingDisplayed = true;

                        /*
                         *   If the actor is the player character, the player
                         *   character now knows of the existence of this object.
                         */
                        if(gActor == gPlayerChar)
                            setKnown(cur);
                    }

                }

                /* Othewise display its smellDesc */
                else
                {
                    if(cur.display(&smellDesc))
                    {
                        somethingDisplayed = true;

                        /*
                         *   If the actor is the player character, the player
                         *   character now knows of the existence of this object.
                         */
                        if(gActor == gPlayerChar)
                            setKnown(cur);
                    }
                }
            }

            /* Tell our caller whether we actually displayed anything */
            return somethingDisplayed;
        }

    ;

        /*
         *   Return a list of items in remote locations that can be heard from the
         *   current actor's location.
         */

    modify Listen
        /*
         *   Return a list of items in remote locations that can be heard from the
         *   current actor's location.
         */
        getRemoteSoundList()
        {
            /* Note the actor's location */
            local loc = gActor.getOutermostRoom;

            /* Create a new vector */
            local vec = new Vector(10);

            /*
             *   Go through all the rooms that can be heard from the actor's
             *   location
             */
            foreach(local rm in loc.audibleRooms)
            {

                /*
                 *   Create a list of objects in the room that can be heard by the
                 *   actor.
                 */
                local sList = rm.allContents.subset({o: Q.canHear(gActor, o)});

                /*
                 *   Create a sublist of that list containing all the items with a
                 *   large soundSize that also define a non-nil listenDesc, plus all
                 *   the items that define a remoteListenDesc, in each case provided
                 *   that the item's isProminentNoise is true. Remove any item (i.e.
                 *   MultiLoc) that's also in the actor's location, so that it's not
                 *   duplicated. Then append this sublist to our vector.
                 */
                vec.appendUnique(sList.subset({o: ((o.soundSize == large &&
                                           o.checkDisplay(&listenDesc) != nil) ||
                                           o.propDefined(&remoteListenDesc))
                                           && o.isProminentNoise
                                              && !o.isOrIsIn(loc)} ));
            }

            /* Convert the vector to a list and return it. */
            return vec.toList();
        }

        /* List smells in remote locations */
        listRemoteSounds(lst)
        {
            local somethingDisplayed = nil;
            local remoteDisplayed = nil;

             /* For each item in lst */
            foreach(local cur in lst)
            {
                /* If the item defined a remoteListenDesc property, display it */
                if(cur.propDefined(&remoteListenDesc))
                {
                    remoteDisplayed = gOutStream.watchForOutput(
                        {:
                        cur.remoteListenDesc(gActor)
                        });

                    if(remoteDisplayed)
                    {
                        somethingDisplayed = true;

                        /*
                         *   If the actor is the player character, the player
                         *   character now knows of the existence of this object.
                         */
                        if(gActor == gPlayerChar)
                            setKnown(cur);
                    }
                }

                /* Othewise display its listenDesc */
                else
                {
                    if(cur.displayAlt(&listenDesc))
                    {
                        somethingDisplayed = true;

                        /*
                         *   If the actor is the player character, the player
                         *   character now knows of the existence of this object.
                         */
                        if(gActor == gPlayerChar)
                            setKnown(cur);
                    }
                }
            }

            /* Tell our caller whether we actually displayed anything */
            return somethingDisplayed;
        }
    ;

    modify SmellSomething
        /* Add any Odors the actor can smell */
        addExtraScopeItems(whichRole?)
        {
            inherited(whichRole);

            local odorList = [];
            for(local rm in gActor.getOutermostRoom.smellableRooms)
                odorList += rm.allContents.subset(
                { o: o.ofKind(Odor) && Q.canSmell(gActor, o) } );

            scopeList = scopeList.appendUnique(odorList);
        }
    ;

    modify ListenTo
        /* Add any Noises the actor can hear */
        addExtraScopeItems(whichRole?)
        {
            inherited(whichRole);

            local noiseList = [];
            for(local rm in gActor.getOutermostRoom.audibleRooms)
                noiseList += rm.allContents.subset(
                { o: o.ofKind(Noise) && Q.canHear(gActor, o) } );

            scopeList = scopeList.appendUnique(noiseList);
        }
    ;

    /*
     *   This Special redefines canHear, canSee, canSmell, canTalkTo and canThrowTo
     *   to take account of possible sensory connections across rooms in a
     *   SenseRegion
     */
    QSenseRegion: Special

        /*
         *   Gives this Special a slightly higher priority than QDefaults, so that
         *   it takes priority.
         */
        priority = 2

        /* This Special should be active whenever this module is included. */
        active = true

        /*
         *   Can A see B?  We return true if and only if B is in light and
         *   there's a clear sight path from A to B.
         */
        canSee(a, b)
        {
            /*
             *   If either a or b is not on the map, assume we can't see from A to
             *   B and return nil (otherwise we'd probably get a run-time error
             *   further down the line). Also A can't see B if B is hidden.
             */
            if(a.isIn(nil) || b.isIn(nil) || b.isHidden)
                return nil;

            /*
             *   Construct a list of objects that might block the sight path from A
             *   to B.
             */
            local blockList = Q.sightBlocker(a, b);

            /*
             *   A can't see B if B isn't in the light or there's something other
             *   than a room blocking the sight path between them
             */
            if(!Q.inLight(b) || blockList.indexWhich({x: !x.ofKind(Room)} ) != nil)
                return nil;

            /*
             *   A can see B if A and B are in the same room or if B is in one of
             *   the rooms in A's room's visibleRoom's list and B can be seen
             *   remotely from A's pov.
             */
            local ar = a.getOutermostRoom(), br = b.getOutermostRoom();

            return b.isOrIsIn(ar) || (b.isVisibleFrom(a) &&
                                 ar.visibleRooms.indexOf(br)
                                      && ar.canSeeOutTo(br)
                                      && br.canSeeInFrom(ar));

        }

        /*
         *   Can A hear B?  We return true if there's a clear sound path from A
         *   to B.
         */
        canHear(a, b)
        {
            /*
             *   If either a or b is not on the map, assume A can't hear B and
             *   return nil (otherwise we'd probably get a run-time error further
             *   down the line.
             */
             if(a.isIn(nil) || b.isIn(nil))
                return nil;

            /*
             *   Construct a list of objects that might block the sound path from A
             *   to B.
             */
            local blockList = Q.soundBlocker(a, b);

            /*
             *   A can't hear B if B there's something other than a room blocking
             *   the sound path between them
             */
            if(blockList.indexWhich({x: !x.ofKind(Room)} ) != nil)
                return nil;

            /*
             *   A can hear B if A and B are in the same room or if B is in one of
             *   the rooms in A's room's audibleRoom's list and B can be heard
             *   remotely from A's pov.
             */
            local ar = a.getOutermostRoom(), br = b.getOutermostRoom();

            return b.isOrIsIn(ar) || (b.isAudibleFrom(a) &&
                                 ar.audibleRooms.indexOf(br)
                                      && ar.canHearOutTo(br)
                                      && br.canHearInFrom(ar));
        }

        /*
         *   Can A smell B?  We return true if there's a clear scent path from
         *   A to B.
         */
        canSmell(a, b)
        {
            /*
             *   If either a or b is not on the map, assume A can't smell B and
             *   return nil (otherwise we'd probably get a run-time error further
             *   down the line.
             */
             if(a.isIn(nil) || b.isIn(nil))
                return nil;

            /*
             *   Construct a list of objects that might block the scent path from A
             *   to B.
             */
             local blockList = Q.scentBlocker(a, b);

            /*
             *   A can't smell B if B there's something other
             *   than a room blocking the scent path between them
             */
            if(blockList.indexWhich({x: !x.ofKind(Room)} ) != nil)
                return nil;

            /*
             *   A can smell B if A and B are in the same room or if B is in one of
             *   the rooms in A's room's smelablelRoom's list and B can be seen
             *   remotely from A's pov.
             */
            local ar = a.getOutermostRoom(), br = b.getOutermostRoom();

            return b.isOrIsIn(ar) || (b.isSmellableFrom(a) &&
                                 ar.smellableRooms.indexOf(br)
                                      && ar.canSmellOutTo(br)
                                      && br.canSmellInFrom(ar));

        }

        /*
         *   For A to be able to talk to B, A must both be able to hear B and either
         *   be in the same room as B or in a room that's close enough to be able to
         *   converse with B
         */

        canTalkTo(a, b)
        {
            /*
             *   If either a or b is not on the map, assume A can't talk to B and
             *   return nil (otherwise we'd probably get a run-time error further
             *   down the line.
             */
            if(a.isIn(nil) || b.isIn(nil))
                return nil;

            /* Note which room A is in */
            local c = a.getOutermostRoom();

            /*
             *   A can talk to B if A can hear B and B's location is in C's list of
             *   rooms where conversation can take place from C, or if B is in C.
             */
            return Q.canHear(a, b)
                && (b.isIn(c)
                    || c.talkableRooms.indexOf(b.getOutermostRoom) != nil);
        }

        /* Can A throw an object to B? */
        canThrowTo(a, b)
        {
            /*
             *   If either a or b is not on the map, assume A can't throw anything
             *   to B and return nil (otherwise we'd probably get a run-time error
             *   further down the line.
             */
            if(a.isIn(nil) || b.isIn(nil))
                return nil;

            /* Note which room A is in */
            local c = a.getOutermostRoom();

            /*
             *   A can throw something to B if B's location is in C's list of rooms
             *   into which objects can be thrown from C, or if B is in C.
             */
            return b.isOrIsIn(c)
                || c.throwableRooms.indexOf(b.getOutermostRoom) != nil;
        }

        /*
         *   Determine if A can reach B, and if not, what stands in the way. Returns
         *   a list of containers along the path between A and B that obstruct the
         *   reach.  If the two objects are in separate rooms, the top-level room
         *   containing A is in the list to represent the room separation.  If
         *   there's no obstruction, we return an empty list.
         */
        reachBlocker(a, b)
        {
            local blockList = a.containerPathBlock(b, &canReachOut, &canReachIn);

            /*
             *   ignore any blocking objects that are Rooms in the same SenseRegion
             *   where the SenseRegion's autoGoTo property is true.
             */
            blockList = blockList.subset({o: nonBlocker(o, b) == nil });

            return blockList;
        }

        /*
         *   Is o the kind of object that would block movement from one room to the
         *   room containing b? It is unless o is a room and the room and b are both
         *   in a SenseRegion for which autoGoTo is true.
         */
        nonBlocker(o, b)
        {
            /* If o isn't a Room, it's something that blocks movement */
            if(!o.ofKind(Room))
                return nil;

            /* Get the Room in which b is located. */
            local bLoc = b.getOutermostRoom;

            /* Get the list of Regions containing both o and bLoc. */
            local regList = o.regionsInCommonWith(bLoc);

            /* Reduce it to the list of Regions for which autoToGo is true */
            regList = regList.subset({r: r.autoGoTo} );

            /*
             *   If this list is not empty, we're a non-blocker so return true,
             *   otherwise return nil
             */
            return regList.length > 0;

        }

        /*
         *   Prepend a ReachProblemRemote to move the actor to the location of the
         *   target object if it's in a remote location, then add any issues we
         *   would have obtained from the next Special in line.
         */
    //    reachProblemCheck(a, b)
    //    {
    //        local issues = [];
    //
    //        if(a.getOutermostRoom != b.getOutermostRoom)
    //            issues += new ReachProblemRemote(a, b);
    //
    //        return issues + next();
    //    }
    ;

    /*
     *   The ReachProblemRemote should only be brought into play when an actor in
     *   one room in a SenseRegion tries to touch an object in another room in the
     *   SenseRegion when the SenseRegion defines autoGoTo as true. The
     *   ReachProblemRemote will then be responsible for trying to move the actor
     *   into the room containing the target object.
     */
    class ReachProblemRemote: ReachProblem
        construct(a, b)
        {
            origRoom = a.getOutermostRoom;
            destRoom = b.getOutermostRoom;
            target = b;
        }

        origRoom = nil
        destRoom = nil
        target = nil

        /*
         *   This check method may be called by the checkPreCondition method of the
         *   touchObj PreCondition. Its effect is to try to move the actor into the
         *   location of the object the actor is trying to touch.
         */
        check(allowImplicit)
        {
            /*
             *   If the actor is already in the location of the target object,
             *   there's nothing to do here. This should never be the case when this
             *   method is called, but we include the check just to be sure.
             */
            if(origRoom == destRoom)
                return true;

            /*
             *   If implicit actions are allowed, try moving the actor to the
             *   destination room (the room containing the object to be touched).
             */
            if(allowImplicit && sayGoTo(destRoom) &&
               tryImplicitAction(GoTo, destRoom))
            {
                /*
                 *   If the actor isn't in the destination room, return nil to tell
                 *   our caller we failed.
                 */
                if(!gActor.isIn(destRoom))
                    return nil;
            }
            else
            {
                /*
                 *   If the implicit GoTo action can't be attempted, simply display
                 *   a message to say that the target object is too far away.
                 */
                say(tooFarAwayMsg);

                /* Then return nil to tell our caller we failed. */
                return nil;
            }

            /*
             *   If we reached this point, all is well, so return true to tell our
             *   caller we succeeded.
             */
            return true;

        }

        /*
         *   The failure message to display if we can't attempt an implicit GoTo
         *   action to move the actor.
         */
        tooFarAwayMsg()
        {
            /* Let the actor's room provide the message. */
            return origRoom.cannotReachTargetMsg(target);
        }

        /*
         *   Report that we are trying to head to destRoom via an implicit action.
         *   We do this separately from the normal implicit action report mechanism
         *   so that this report is display before travel begins, not at the end of
         *   it as it otherwise would be.
         */
        sayGoTo(destRoom)
        {
            DMsg(implicit go, '(first heading for {1})\n', destRoom.theName);
            return true;
        }
    ;

    modify touchObj
        checkPreCondition(obj, allowImplicit)
        {
            /*
             *   We first try moving to a new room as a separate exercise before
             *   checking for any other obstacles, since if the move is successful
             *   it may change the status of other potential obstacles.
             */
            local remoteCheck = new ReachProblemRemote(gActor, obj);

            if(!remoteCheck.check(allowImplicit))
                return nil;

            return inherited(obj, allowImplicit);
        }
    ;

Adv3Lite Library Reference Manual
Generated on 01/03/2024 from adv3Lite version 1.6.2